diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 8f17cbf5..82603f6d 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2580,30 +2580,12 @@ parameters: count: 1 path: tests/lib/RichText/Converter/LinkTest.php - - - message: '#^Parameter \#1 \$locationService of class Ibexa\\FieldTypeRichText\\RichText\\Converter\\Link constructor expects Ibexa\\Contracts\\Core\\Repository\\LocationService, PHPUnit\\Framework\\MockObject\\MockObject given\.$#' - identifier: argument.type - count: 5 - path: tests/lib/RichText/Converter/LinkTest.php - - - - message: '#^Parameter \#2 \$contentService of class Ibexa\\FieldTypeRichText\\RichText\\Converter\\Link constructor expects Ibexa\\Contracts\\Core\\Repository\\ContentService, PHPUnit\\Framework\\MockObject\\MockObject given\.$#' - identifier: argument.type - count: 5 - path: tests/lib/RichText/Converter/LinkTest.php - - message: '#^Parameter \#2 \$function of class Ibexa\\Core\\Base\\Exceptions\\UnauthorizedException constructor expects string, int given\.$#' identifier: argument.type count: 4 path: tests/lib/RichText/Converter/LinkTest.php - - - message: '#^Parameter \#3 \$router of class Ibexa\\FieldTypeRichText\\RichText\\Converter\\Link constructor expects Symfony\\Component\\Routing\\RouterInterface, PHPUnit\\Framework\\MockObject\\MockObject given\.$#' - identifier: argument.type - count: 5 - path: tests/lib/RichText/Converter/LinkTest.php - - message: '#^Call to method expects\(\) on an unknown class Ibexa\\FieldTypeRichText\\RichText\\RendererInterface\.$#' identifier: class.notFound diff --git a/src/bundle/Resources/public/js/CKEditor/embed/image/embed-image-editing.js b/src/bundle/Resources/public/js/CKEditor/embed/image/embed-image-editing.js index 79eebb80..643eaeee 100644 --- a/src/bundle/Resources/public/js/CKEditor/embed/image/embed-image-editing.js +++ b/src/bundle/Resources/public/js/CKEditor/embed/image/embed-image-editing.js @@ -77,7 +77,7 @@ class IbexaEmbedImageEditing extends Plugin { defineSchema() { const { schema } = this.editor.model; const customClassesConfig = getCustomClassesConfig(); - const allowedAttributes = ['contentId', 'size', 'ibexaLinkHref', 'ibexaLinkTitle', 'ibexaLinkTarget']; + const allowedAttributes = ['contentId', 'size', 'ibexaLinkHref', 'ibexaLinkTitle', 'ibexaLinkTarget', 'ibexaLinkSiteaccess']; if (customClassesConfig.link) { allowedAttributes.push('ibexaLinkClasses'); diff --git a/src/bundle/Resources/public/js/CKEditor/link/link-command.js b/src/bundle/Resources/public/js/CKEditor/link/link-command.js index a889b379..38416aa7 100644 --- a/src/bundle/Resources/public/js/CKEditor/link/link-command.js +++ b/src/bundle/Resources/public/js/CKEditor/link/link-command.js @@ -35,6 +35,7 @@ class IbexaLinkCommand extends Command { writer.setAttribute('ibexaLinkHref', linkData.href, element); writer.setAttribute('ibexaLinkTitle', linkData.title, element); writer.setAttribute('ibexaLinkTarget', linkData.target, element); + writer.setAttribute('ibexaLinkSiteaccess', linkData.siteaccess, element); writer.setAttribute('ibexaLinkClasses', linkData.ibexaLinkClasses, element); if (linkData.ibexaLinkAttributes) { diff --git a/src/bundle/Resources/public/js/CKEditor/link/link-editing.js b/src/bundle/Resources/public/js/CKEditor/link/link-editing.js index 68621041..978af315 100644 --- a/src/bundle/Resources/public/js/CKEditor/link/link-editing.js +++ b/src/bundle/Resources/public/js/CKEditor/link/link-editing.js @@ -41,6 +41,16 @@ class IbexaCustomTagEditing extends Plugin { view: (target, { writer: downcastWriter }) => downcastWriter.createAttributeElement('a', { target }), }); + conversion.for('editingDowncast').attributeToElement({ + model: 'ibexaLinkSiteaccess', + view: (siteaccess, { writer: downcastWriter }) => downcastWriter.createAttributeElement('a', { siteaccess }), + }); + + conversion.for('dataDowncast').attributeToElement({ + model: 'ibexaLinkSiteaccess', + view: (siteaccess, { writer: downcastWriter }) => downcastWriter.createAttributeElement('a', { siteaccess }), + }); + if (customClassesLinkConfig) { conversion.for('editingDowncast').attributeToElement({ model: 'ibexaLinkClasses', @@ -77,6 +87,8 @@ class IbexaCustomTagEditing extends Plugin { const ibexaLinkHref = data.viewItem.getAttribute('href'); const ibexaLinkTitle = data.viewItem.getAttribute('title'); const ibexaLinkTarget = data.viewItem.getAttribute('target'); + const ibexaLinkSiteaccess = data.viewItem.getAttribute('siteaccess'); + const classes = data.viewItem.getAttribute('class'); conversionApi.writer.setAttributes( @@ -84,6 +96,7 @@ class IbexaCustomTagEditing extends Plugin { ibexaLinkHref, ibexaLinkTitle, ibexaLinkTarget, + ibexaLinkSiteaccess, }, data.modelRange, ); @@ -115,6 +128,7 @@ class IbexaCustomTagEditing extends Plugin { this.editor.model.schema.extend('$text', { allowAttributes: 'ibexaLinkHref' }); this.editor.model.schema.extend('$text', { allowAttributes: 'ibexaLinkTitle' }); this.editor.model.schema.extend('$text', { allowAttributes: 'ibexaLinkTarget' }); + this.editor.model.schema.extend('$text', { allowAttributes: 'ibexaLinkSiteaccess' }); if (customAttributesLinkConfig) { const attributes = Object.keys(customAttributesLinkConfig); diff --git a/src/bundle/Resources/public/js/CKEditor/link/link-ui.js b/src/bundle/Resources/public/js/CKEditor/link/link-ui.js index 4fb0efdc..5a773a22 100644 --- a/src/bundle/Resources/public/js/CKEditor/link/link-ui.js +++ b/src/bundle/Resources/public/js/CKEditor/link/link-ui.js @@ -36,7 +36,7 @@ class IbexaLinkUI extends Plugin { const formView = new IbexaLinkFormView({ locale: this.editor.locale, editor: this.editor }); this.listenTo(formView, 'save-link', () => { - const { url, title, target, ibexaLinkClasses, ibexaLinkAttributes } = this.formView.getValues(); + const { url, title, target, ibexaLinkClasses, ibexaLinkAttributes, siteaccess } = this.formView.getValues(); const { path: firstPosition } = this.editor.model.document.selection.getFirstPosition(); const { path: lastPosition } = this.editor.model.document.selection.getLastPosition(); const noRangeSelection = firstPosition[0] === lastPosition[0] && firstPosition[1] === lastPosition[1]; @@ -59,7 +59,7 @@ class IbexaLinkUI extends Plugin { this.isNew = false; - this.editor.execute('insertIbexaLink', { href: url, title, target, ibexaLinkClasses, ibexaLinkAttributes }); + this.editor.execute('insertIbexaLink', { href: url, title, target, siteaccess, ibexaLinkClasses, ibexaLinkAttributes }); this.hideForm(); }); @@ -78,6 +78,7 @@ class IbexaLinkUI extends Plugin { writer.removeAttribute('ibexaLinkHref', element); writer.removeAttribute('ibexaLinkTitle', element); writer.removeAttribute('ibexaLinkTarget', element); + writer.removeAttribute('ibexaLinkSiteaccess', element); if (customClassesLinkConfig) { writer.removeAttribute('ibexaLinkClasses', element); @@ -121,6 +122,7 @@ class IbexaLinkUI extends Plugin { url: link?.getAttribute('href') ?? '', title: link?.getAttribute('title') ?? '', target: link?.getAttribute('target') ?? '', + siteaccess: link?.getAttribute('siteaccess') ?? '', }; if (customClassesLinkConfig) { @@ -172,7 +174,7 @@ class IbexaLinkUI extends Plugin { if (!link) { this.editor.focus(); - this.editor.execute('insertIbexaLink', { href: '', title: '', target: '' }); + this.editor.execute('insertIbexaLink', { href: '', title: '', target: '', siteaccess: '' }); this.isNew = true; } diff --git a/src/bundle/Resources/public/js/CKEditor/link/ui/link-form-view.js b/src/bundle/Resources/public/js/CKEditor/link/ui/link-form-view.js index 647046ed..76c9b6c0 100644 --- a/src/bundle/Resources/public/js/CKEditor/link/ui/link-form-view.js +++ b/src/bundle/Resources/public/js/CKEditor/link/ui/link-form-view.js @@ -13,6 +13,8 @@ import { createLabeledInputNumber } from '../../common/input-number/utils'; import { addMultivalueSupport } from '../../common/multivalue-dropdown/utils'; import { getCustomAttributesConfig, getCustomClassesConfig } from '../../custom-attributes/helpers/config-helper'; +const { ibexa } = window; + class IbexaLinkFormView extends View { constructor(props) { super(props); @@ -27,6 +29,7 @@ class IbexaLinkFormView extends View { this.urlInputView = this.createTextInput({ label: 'Link to' }); this.titleView = this.createTextInput({ label: 'Title' }); this.targetSwitcherView = this.createBoolean({ label: 'Open in tab' }); + this.siteAccessView = this.createDropdown({ label: 'Site access', choices: [] }); this.attributeRenderMethods = { string: this.createTextInput, number: this.createNumberInput, @@ -128,9 +131,10 @@ class IbexaLinkFormView extends View { this.listenTo(this.selectContentButtonView, 'execute', this.chooseContent); } - setValues({ url, title, target, ibexaLinkClasses, ibexaLinkAttributes = {} }) { + setValues({ url, title, target, siteaccess, ibexaLinkClasses, ibexaLinkAttributes = {} }) { this.setStringValue(this.urlInputView, url); this.setStringValue(this.titleView, title); + this.setChoiceValue(this.siteAccessView, siteaccess); this.targetSwitcherView.fieldView.element.value = !!target; this.targetSwitcherView.fieldView.set('value', !!target); @@ -141,6 +145,12 @@ class IbexaLinkFormView extends View { this.setChoiceValue(this.classesView, ibexaLinkClasses); } + if (url.includes('ezlocation://')) { + const locationId = url.replace('ezlocation://', ''); + + this.fetchSiteaccesses(locationId); + } + Object.entries(ibexaLinkAttributes).forEach(([name, value]) => { const attributeView = this.attributeViews[`ibexaLink${name}`]; const setValueMethod = this.setValueMethods[this.customAttributes[name].type]; @@ -187,6 +197,7 @@ class IbexaLinkFormView extends View { url, title: this.titleView.fieldView.element.value, target: this.targetSwitcherView.fieldView.isOn ? '_blank' : '', + siteaccess: this.siteAccessView.fieldView.element.value, }; const customClassesValue = this.classesView?.fieldView.element.value; const customAttributesValue = Object.entries(this.attributeViews).reduce((output, [name, view]) => { @@ -264,19 +275,17 @@ class IbexaLinkFormView extends View { children.add(this.selectContentButtonView); children.add(this.urlInputView); + children.add(this.siteAccessView); children.add(this.titleView); children.add(this.targetSwitcherView); return children; } - createDropdown(config, isCustomAttribute = false) { + createDropdownItemsList(config) { const Translator = getTranslator(); - const labeledDropdown = new LabeledFieldView(this.locale, createLabeledDropdown); const itemsList = new Collection(); - labeledDropdown.label = config.label; - if (!config.multiple && !config.required) { itemsList.add({ type: 'button', @@ -299,6 +308,15 @@ class IbexaLinkFormView extends View { }); }); + return itemsList; + } + + createDropdown(config, isCustomAttribute = false) { + const labeledDropdown = new LabeledFieldView(this.locale, createLabeledDropdown); + const itemsList = this.createDropdownItemsList(config); + + labeledDropdown.label = config.label; + addListToDropdown(labeledDropdown.fieldView, itemsList); if (config.multiple) { @@ -463,12 +481,42 @@ class IbexaLinkFormView extends View { this.urlInputView.fieldView.set('value', url); this.urlInputView.fieldView.set('isEmpty', !url); + this.fetchSiteaccesses(items[0].id); + this.editor.focus(); } cancelHandler() { this.editor.focus(); } + + fetchSiteaccesses(locationId) { + const request = new Request(`/api/ibexa/v2/site-access/by-location/${locationId}?resolver_type=non_admin`, { + method: 'GET', + headers: { + Accept: 'application/json', + }, + mode: 'same-origin', + credentials: 'same-origin', + }); + + fetch(request) + .then(ibexa.helpers.request.getJsonFromResponse) + .then((response) => { + const itemsList = this.createDropdownItemsList({ + choices: response.SiteAccessesList.values.map((siteaccess) => siteaccess.name), + }); + + this.siteAccessView.fieldView.once( + 'change:isOpen', + () => { + this.siteAccessView.fieldView.panelView.children.clear(); + addListToDropdown(this.siteAccessView.fieldView, itemsList); + }, + { priority: 'highest' }, + ); + }); + } } export default IbexaLinkFormView; diff --git a/src/bundle/Resources/public/scss/_balloon-form.scss b/src/bundle/Resources/public/scss/_balloon-form.scss index aaa8ba43..130390fa 100644 --- a/src/bundle/Resources/public/scss/_balloon-form.scss +++ b/src/bundle/Resources/public/scss/_balloon-form.scss @@ -28,7 +28,6 @@ border: calculateRem(1px) solid $ibexa-color-dark-200; border-radius: calculateRem(5px); width: 100%; - max-width: calculateRem(288px); .ibexa-ckeditor-dropdown-selected-items { display: flex; diff --git a/src/bundle/Resources/richtext/schemas/docbook/docbook.rng b/src/bundle/Resources/richtext/schemas/docbook/docbook.rng index 019c84b8..9f4885e0 100644 --- a/src/bundle/Resources/richtext/schemas/docbook/docbook.rng +++ b/src/bundle/Resources/richtext/schemas/docbook/docbook.rng @@ -252,6 +252,13 @@ + + + + Specifies the siteaccess for the location + + + onLoad @@ -291,6 +298,9 @@ + + + @@ -6878,6 +6888,9 @@ O, the molecular formula for water) + + + diff --git a/src/bundle/Resources/richtext/stylesheets/docbook/xhtml5/edit/core.xsl b/src/bundle/Resources/richtext/stylesheets/docbook/xhtml5/edit/core.xsl index a08cc6c3..db6dc7c9 100644 --- a/src/bundle/Resources/richtext/stylesheets/docbook/xhtml5/edit/core.xsl +++ b/src/bundle/Resources/richtext/stylesheets/docbook/xhtml5/edit/core.xsl @@ -253,6 +253,11 @@ + + + + + @@ -660,6 +665,11 @@ + + + + + diff --git a/src/bundle/Resources/richtext/stylesheets/docbook/xhtml5/output/core.xsl b/src/bundle/Resources/richtext/stylesheets/docbook/xhtml5/output/core.xsl index d94d8af5..1d6ef99f 100644 --- a/src/bundle/Resources/richtext/stylesheets/docbook/xhtml5/output/core.xsl +++ b/src/bundle/Resources/richtext/stylesheets/docbook/xhtml5/output/core.xsl @@ -255,6 +255,11 @@ + + + + + diff --git a/src/bundle/Resources/richtext/stylesheets/xhtml5/edit/docbook.xsl b/src/bundle/Resources/richtext/stylesheets/xhtml5/edit/docbook.xsl index e2031689..b6564e94 100644 --- a/src/bundle/Resources/richtext/stylesheets/xhtml5/edit/docbook.xsl +++ b/src/bundle/Resources/richtext/stylesheets/xhtml5/edit/docbook.xsl @@ -284,6 +284,11 @@ + + + + + diff --git a/src/lib/RichText/Converter/Link.php b/src/lib/RichText/Converter/Link.php index bb8c246c..c56b4e1a 100644 --- a/src/lib/RichText/Converter/Link.php +++ b/src/lib/RichText/Converter/Link.php @@ -18,6 +18,7 @@ use Ibexa\Contracts\FieldTypeRichText\RichText\Converter; use Ibexa\Core\MVC\Symfony\Routing\UrlAliasRouter; use Psr\Log\LoggerInterface; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\RouterInterface; class Link implements Converter @@ -77,6 +78,7 @@ public function convert(DOMDocument $document) // Set resolved href to number character as a default if it can't be resolved $hrefResolved = '#'; $href = $link->getAttribute('xlink:href'); + $siteaccess = $link->getAttribute('xlink:siteaccess') ?? null; $location = null; preg_match('~^(.+://)?([^#]*)?(#.*|\\s*)?$~', $href, $matches); list(, $scheme, $id, $fragment) = $matches; @@ -85,7 +87,7 @@ public function convert(DOMDocument $document) try { $contentInfo = $this->contentService->loadContentInfo((int) $id); $location = $this->locationService->loadLocation($contentInfo->mainLocationId); - $hrefResolved = $this->generateUrlAliasForLocation($location, $fragment); + $hrefResolved = $this->generateUrlAliasForLocation($location, $fragment, $siteaccess); } catch (APINotFoundException $e) { if ($this->logger) { $this->logger->warning( @@ -104,7 +106,7 @@ public function convert(DOMDocument $document) } elseif ($scheme === 'ezlocation://') { try { $location = $this->locationService->loadLocation((int) $id); - $hrefResolved = $this->generateUrlAliasForLocation($location, $fragment); + $hrefResolved = $this->generateUrlAliasForLocation($location, $fragment, $siteaccess); } catch (APINotFoundException $e) { if ($this->logger) { $this->logger->warning( @@ -140,11 +142,20 @@ public function convert(DOMDocument $document) return $document; } - private function generateUrlAliasForLocation(Location $location, string $fragment): string - { + private function generateUrlAliasForLocation( + Location $location, + string $fragment, + ?string $siteaccess + ): string { + $params = ['location' => $location]; + if (!empty($siteaccess)) { + $params['siteaccess'] = $siteaccess; + } + $urlAlias = $this->router->generate( UrlAliasRouter::URL_ALIAS_ROUTE_NAME, - ['location' => $location] + $params, + UrlGeneratorInterface::ABSOLUTE_URL ); return $urlAlias . $fragment; diff --git a/src/lib/RichText/Converter/Render/Embed.php b/src/lib/RichText/Converter/Render/Embed.php index a695c5c9..ab00e4ee 100644 --- a/src/lib/RichText/Converter/Render/Embed.php +++ b/src/lib/RichText/Converter/Render/Embed.php @@ -193,6 +193,7 @@ protected function extractLinkParameters(DOMElement $embed) $title = $link->getAttribute('xlink:title'); $id = $link->getAttribute('xml:id'); $class = $link->getAttribute('ezxhtml:class'); + $siteAccess = $link->getAttribute('xlink:siteaccess'); if (strpos($href, 'ezcontent://') === 0) { $resourceType = 'CONTENT'; diff --git a/tests/lib/RichText/Converter/LinkTest.php b/tests/lib/RichText/Converter/LinkTest.php index 5cdb3865..a6e3e27c 100644 --- a/tests/lib/RichText/Converter/LinkTest.php +++ b/tests/lib/RichText/Converter/LinkTest.php @@ -28,7 +28,7 @@ class LinkTest extends TestCase { /** - * @return \PHPUnit\Framework\MockObject\MockObject + * @return \PHPUnit\Framework\MockObject\MockObject&\Ibexa\Contracts\Core\Repository\ContentService */ protected function getMockContentService() { @@ -36,7 +36,7 @@ protected function getMockContentService() } /** - * @return \PHPUnit\Framework\MockObject\MockObject + * @return \PHPUnit\Framework\MockObject\MockObject&\Ibexa\Contracts\Core\Repository\LocationService */ protected function getMockLocationService() { @@ -44,7 +44,7 @@ protected function getMockLocationService() } /** - * @return \PHPUnit\Framework\MockObject\MockObject + * @return \PHPUnit\Framework\MockObject\MockObject&\Symfony\Component\Routing\RouterInterface */ protected function getMockRouter() { @@ -144,7 +144,7 @@ public function testLink($input, $output) /** * @return array */ - public function providerLocationLink() + public function providerLocationLink(): array { return [ [ @@ -197,6 +197,24 @@ public function providerLocationLink() +', + 106, + 'test', + ], + [ + ' +
+ Link example with empty site access + + + +
', + ' +
+ Link example with empty site access + + +
', 106, 'test', @@ -240,10 +258,61 @@ public function testConvertLocationLink($input, $output, $locationId, $urlResolv $this->assertEquals($expectedOutputDocument, $outputDocument); } + /** + * Test conversion of ezlocation:// links with the 'siteaccess' attribute. + */ + public function testConvertLocationLinkWithSiteAccess(): void + { + $inputDocument = new DOMDocument(); + $input = ' +
+ Link example with site access + + + +
'; + $output = ' +
+ Link example with site access + + + +
'; + $inputDocument->loadXML($input); + + $contentService = $this->getMockContentService(); + $locationService = $this->getMockLocationService(); + $router = $this->getMockRouter(); + + $location = $this->createMock(APILocation::class); + + $locationService->expects(self::once()) + ->method('loadLocation') + ->with(self::equalTo(106)) + ->willReturn($location); + + $router->expects(self::once()) + ->method('generate') + ->with(UrlAliasRouter::URL_ALIAS_ROUTE_NAME, [ + 'location' => $location, + 'siteaccess' => 'site', + ]) + ->willReturn('test'); + + $converter = new Link($locationService, $contentService, $router); + + $outputDocument = $converter->convert($inputDocument); + + $expectedOutputDocument = new DOMDocument(); + $expectedOutputDocument->loadXML($output); + + $this->assertEquals($expectedOutputDocument, $outputDocument); + } + /** * @return array */ - public function providerBadLocationLink() + public function providerBadLocationLink(): array { return [ [ diff --git a/tests/lib/RichText/Converter/Xslt/_fixtures/docbook/014-link.xml b/tests/lib/RichText/Converter/Xslt/_fixtures/docbook/014-link.xml index f0897850..8ddeaa03 100644 --- a/tests/lib/RichText/Converter/Xslt/_fixtures/docbook/014-link.xml +++ b/tests/lib/RichText/Converter/Xslt/_fixtures/docbook/014-link.xml @@ -20,6 +20,9 @@ Content name + + Location content name + diff --git a/tests/lib/RichText/Converter/Xslt/_fixtures/xhtml5/edit/014-link.xml b/tests/lib/RichText/Converter/Xslt/_fixtures/xhtml5/edit/014-link.xml index 51155621..e165ec49 100644 --- a/tests/lib/RichText/Converter/Xslt/_fixtures/xhtml5/edit/014-link.xml +++ b/tests/lib/RichText/Converter/Xslt/_fixtures/xhtml5/edit/014-link.xml @@ -16,6 +16,9 @@

Content name

+

+ Location content name +

Content name

diff --git a/tests/lib/RichText/Converter/Xslt/_fixtures/xhtml5/output/014-link.xml b/tests/lib/RichText/Converter/Xslt/_fixtures/xhtml5/output/014-link.xml index 70689f5c..78517dfa 100644 --- a/tests/lib/RichText/Converter/Xslt/_fixtures/xhtml5/output/014-link.xml +++ b/tests/lib/RichText/Converter/Xslt/_fixtures/xhtml5/output/014-link.xml @@ -16,6 +16,9 @@

Content name

+

+ Location content name +

Content name