├── logs
└── .gitkeep
├── tests
├── Fixture
│ └── empty
├── files
│ ├── empty.csv
│ ├── input.txt
│ ├── projects
│ │ ├── undefined.php
│ │ └── test.php
│ ├── test.png
│ ├── test2.png
│ ├── import.csv
│ └── manifest.json
├── Utils
│ ├── MyDummyImportFilter.php
│ ├── ImportFilterSampleError.php
│ ├── ImportFilterSample.php
│ ├── DummyImportFilter.php
│ └── ModulesControllerSample.php
├── TestCase
│ ├── Controller
│ │ ├── CustomBulkAction.php
│ │ ├── CourtesyPageControllerTest.php
│ │ ├── Admin
│ │ │ └── CacheControllerTest.php
│ │ ├── ErrorControllerTest.php
│ │ └── MultiuploadControllerTest.php
│ ├── Utility
│ │ ├── ApiClientTraitTest.php
│ │ └── RelationsToolsTest.php
│ ├── PluginTest.php
│ ├── View
│ │ └── Helper
│ │ │ └── DatesHelperTest.php
│ └── Core
│ │ └── I18n
│ │ └── DummyTranslator.php
├── .env.example
└── bootstrap.php
├── config
├── projects
│ └── .gitkeep
├── version.ini
├── bedita-api-version.ini
├── relations.php
├── schema
│ ├── i18n.sql
│ └── sessions.sql
├── requirements.php
└── oembed.php
├── webroot
├── bundle-report
│ └── .gitkeep
├── robots.txt
├── js
│ └── libs
│ │ ├── README
│ │ └── timezone.js
├── favicon.ico
├── favicon.png
├── fonts
│ ├── be-icons.eot
│ ├── be-icons.ttf
│ ├── be-icons.woff
│ └── be-icons.woff2
├── img
│ └── iconLocked.png
├── .htaccess
├── svg
│ ├── iconLocked.svg
│ ├── iconDraft.svg
│ ├── iconFuture.svg
│ ├── iconExpired.svg
│ ├── concurrent-editors.svg
│ └── iconFloppy.svg
└── index.php
├── templates
├── Layout
│ ├── ajax.twig
│ ├── rss
│ │ └── default.ctp
│ └── Email
│ │ ├── text
│ │ └── default.ctp
│ │ └── html
│ │ └── default.ctp
├── CourtesyPage
│ └── index.twig
├── Element
│ ├── Dashboard
│ │ └── messages.twig
│ ├── flash
│ │ ├── info.twig
│ │ ├── error.twig
│ │ ├── success.twig
│ │ ├── warning.twig
│ │ └── flash.twig
│ ├── Form
│ │ ├── empty.twig
│ │ ├── locations.twig
│ │ ├── dropupload.twig
│ │ ├── publish_properties.scss
│ │ ├── categories.scss
│ │ ├── custom_left.twig
│ │ ├── custom_right.twig
│ │ ├── title.twig
│ │ ├── calendar.scss
│ │ ├── core_properties.twig
│ │ ├── publish_properties.twig
│ │ ├── advanced_properties.twig
│ │ ├── fast_create.twig
│ │ ├── bulk_trash.twig
│ │ ├── bulk_category.twig
│ │ ├── permissions.twig
│ │ ├── multiupload.twig
│ │ ├── history.twig
│ │ ├── tags.twig
│ │ ├── group_properties.twig
│ │ ├── meta.twig
│ │ ├── other_properties.twig
│ │ ├── resource_relations.twig
│ │ ├── categories.twig
│ │ ├── calendar.twig
│ │ ├── form_file_upload.twig
│ │ ├── history.scss
│ │ ├── bulk_custom.twig
│ │ ├── captions.twig
│ │ ├── bulk_position.twig
│ │ └── map.twig
│ ├── Modules
│ │ ├── index_properties_date_ranges.twig
│ │ ├── tree.twig
│ │ ├── list.twig
│ │ ├── index_properties.scss
│ │ ├── index_bulk.twig
│ │ └── index_header.twig
│ ├── json_meta_config.twig
│ ├── Menu
│ │ ├── colophon.scss
│ │ └── colophon.twig
│ ├── FilterBox
│ │ └── filter_box.twig
│ ├── Panel
│ │ ├── panel.twig
│ │ └── panel.scss
│ ├── Model
│ │ ├── relation_types.twig
│ │ └── sidebar_links.twig
│ ├── Admin
│ │ └── sidebar.twig
│ └── custom_colors.twig
├── Pages
│ ├── Translations
│ │ ├── add.twig
│ │ ├── edit.twig
│ │ └── view.scss
│ ├── Multiupload
│ │ └── index.twig
│ ├── Admin
│ │ ├── AsyncJobs
│ │ │ └── index.twig
│ │ ├── Config
│ │ │ └── index.twig
│ │ ├── Endpoints
│ │ │ └── index.twig
│ │ ├── Roles
│ │ │ └── index.twig
│ │ ├── Applications
│ │ │ └── index.twig
│ │ ├── AuthProviders
│ │ │ └── index.twig
│ │ ├── ExternalAuth
│ │ │ └── index.twig
│ │ ├── EndpointPermissions
│ │ │ └── index.twig
│ │ ├── UserAccesses
│ │ │ └── index.twig
│ │ ├── ObjectsHistory
│ │ │ └── index.twig
│ │ ├── Statistics
│ │ │ └── index.twig
│ │ ├── Appearance
│ │ │ └── index.twig
│ │ ├── SystemInfo
│ │ │ └── index.twig
│ │ └── _admin.scss
│ ├── Model
│ │ ├── ObjectTypes
│ │ │ └── index.twig
│ │ ├── Categories
│ │ │ └── index.twig
│ │ ├── Tags
│ │ │ └── index.twig
│ │ └── _model-view.scss
│ ├── Password
│ │ ├── request_sent.twig
│ │ └── change.twig
│ ├── Modules
│ │ ├── setup.twig
│ │ └── index.twig
│ ├── Categories
│ │ └── index.twig
│ └── Import
│ │ ├── index.twig
│ │ └── import.scss
├── Email
│ ├── text
│ │ └── default.ctp
│ └── html
│ │ └── default.ctp
└── Error
│ ├── error400.twig
│ └── error500.twig
├── phpstan.neon.dist
├── .htaccess
├── .dockerignore
├── COPYING
├── resources
├── styles
│ ├── _mixins.scss
│ ├── _non-production.scss
│ └── _base.scss
└── js
│ ├── config
│ └── locales.js
│ ├── app
│ ├── pages
│ │ ├── trash
│ │ │ ├── view.js
│ │ │ └── index.js
│ │ ├── admin
│ │ │ └── index.js
│ │ └── dashboard
│ │ │ └── index.js
│ ├── components
│ │ ├── tag-form
│ │ │ └── tag-form.scss
│ │ ├── event-bus.vue
│ │ ├── charts
│ │ │ └── bar-chart.vue
│ │ ├── form
│ │ │ ├── field-title.vue
│ │ │ ├── field-string.vue
│ │ │ ├── field-plaintext.vue
│ │ │ ├── field-integer.vue
│ │ │ ├── field-number.vue
│ │ │ ├── field-checkbox.vue
│ │ │ ├── field-date.vue
│ │ │ ├── field-json.vue
│ │ │ ├── field-select.vue
│ │ │ ├── field-radio.vue
│ │ │ └── field-textarea.vue
│ │ ├── horizontal-tab-view.js
│ │ ├── email-input.js
│ │ ├── thumbnail
│ │ │ └── thumbnail.vue
│ │ ├── autosize-textarea.js
│ │ ├── clipboard-item
│ │ │ └── clipboard-item.vue
│ │ ├── object-types-list
│ │ │ └── object-types-list.js
│ │ ├── permission
│ │ │ └── permission.vue
│ │ ├── form-file-upload.js
│ │ ├── secret
│ │ │ └── secret.js
│ │ ├── json-editor
│ │ │ └── json-editor.vue
│ │ ├── menu.js
│ │ ├── staggered-list.js
│ │ ├── object-nav
│ │ │ └── object-nav.vue
│ │ └── show-hide
│ │ │ └── show-hide.vue
│ ├── locales.js
│ ├── directives
│ │ ├── uri.js
│ │ ├── email.js
│ │ └── jsoneditor.js
│ ├── helpers
│ │ └── text-helper.js
│ └── mixins
│ │ └── fetch.js
│ └── libs
│ ├── filters.js
│ ├── bedita.js
│ └── urlUtils.js
├── webpack-gettext-loader.js
├── .scrutinizer.yml
├── bin
├── cake.php
├── cake.bat
├── perms.sh
└── bash_completion.sh
├── .editorconfig
├── phpcs.xml.dist
├── src
├── View
│ ├── Helper
│ │ ├── DatesHelper.php
│ │ └── EditorsHelper.php
│ └── AjaxView.php
├── Controller
│ ├── Admin
│ │ ├── UserAccessesController.php
│ │ ├── ObjectsHistoryController.php
│ │ ├── EndpointsController.php
│ │ ├── CacheController.php
│ │ ├── ApplicationsController.php
│ │ ├── AsyncJobsController.php
│ │ └── AuthProvidersController.php
│ ├── CourtesyPageController.php
│ ├── Component
│ │ └── ParentsComponent.php
│ ├── Model
│ │ └── ExportController.php
│ └── MultiuploadController.php
├── Form
│ ├── CustomHandlerInterface.php
│ └── Form.php
├── Core
│ ├── Bulk
│ │ └── CustomBulkActionInterface.php
│ └── Result
│ │ └── ImportResult.php
├── Utility
│ ├── System.php
│ ├── Schema.php
│ ├── ApiClientTrait.php
│ ├── Translate.php
│ └── RelationsTools.php
├── Plugin.php
└── Middleware
│ └── ConfigurationMiddleware.php
├── .github
├── dependabot.yml
└── workflows
│ ├── javascript.yml
│ └── php.yml
├── index.php
├── psalm.xml
├── phpunit.xml.dist
├── .gitignore
└── Dockerfile
/logs/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/Fixture/empty:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/files/empty.csv:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/config/projects/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/webroot/bundle-report/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/files/input.txt:
--------------------------------------------------------------------------------
1 | Some text in here
2 |
--------------------------------------------------------------------------------
/webroot/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /
--------------------------------------------------------------------------------
/config/version.ini:
--------------------------------------------------------------------------------
1 | [Manager]
2 | version=5.17.2
3 |
--------------------------------------------------------------------------------
/templates/Layout/ajax.twig:
--------------------------------------------------------------------------------
1 | {{ fetch('content') }}
2 |
--------------------------------------------------------------------------------
/templates/CourtesyPage/index.twig:
--------------------------------------------------------------------------------
1 |
component used for TrashPage -> View
6 | *
7 | * @extends ModulesView
8 | */
9 |
10 | import ModulesView from 'app/pages/modules/view';
11 |
12 | export default {
13 | extends: ModulesView,
14 | }
15 |
--------------------------------------------------------------------------------
/resources/js/app/components/tag-form/tag-form.scss:
--------------------------------------------------------------------------------
1 | .tags-container {
2 | margin-bottom: 2rem;
3 | .tags {
4 | .list-objects {
5 | .disabled {
6 | color: gray;
7 | }
8 |
9 | input[type=checkbox]:disabled {
10 | filter: invert(1) brightness(3);
11 | }
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/.scrutinizer.yml:
--------------------------------------------------------------------------------
1 | checks:
2 | php: true
3 | filter:
4 | paths:
5 | - 'src/*'
6 | dependency_paths:
7 | - 'vendor/*'
8 |
9 | build:
10 | image: default-jammy
11 | environment:
12 | node: v22
13 | nodes:
14 | analysis:
15 | environment:
16 | php:
17 | version: 8.3.3
18 | tests:
19 | override:
20 | - php-scrutinizer-run
21 |
--------------------------------------------------------------------------------
/tests/TestCase/Controller/CustomBulkAction.php:
--------------------------------------------------------------------------------
1 |
10 |
11 |
--------------------------------------------------------------------------------
/templates/Element/Form/locations.twig:
--------------------------------------------------------------------------------
1 | {# Locations: show add relation form if the relation is set #}
2 |
8 | {% do Form.unlockField('relations.' ~ relationName ~ '.replaceRelated') %}
9 |
--------------------------------------------------------------------------------
/templates/Element/Form/dropupload.twig:
--------------------------------------------------------------------------------
1 | {# :accepted-drop="[`.from-relation-${relationName}`,isRelationWithMedia && 'from-files']"> #}
2 | {% set ot = objectTypes|length == 1 ? objectTypes[0] : 'media' %}
3 |
9 |
10 |
--------------------------------------------------------------------------------
/templates/Element/Menu/colophon.scss:
--------------------------------------------------------------------------------
1 | .menu-colophon {
2 | padding: $gutter * 1.5 0 0;
3 | border-top: 1px solid #495057;
4 | color: $gray-500;
5 | font-size: 12px;
6 | font-weight: 100;
7 |
8 | @media (min-width: 568px) {
9 | padding: 0;
10 | border: none;
11 | }
12 |
13 | span {
14 | display: block;
15 | }
16 | span.error {
17 | color: red;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/bin/cake.php:
--------------------------------------------------------------------------------
1 | #!/usr/bin/php -q
2 | run($argv));
13 |
--------------------------------------------------------------------------------
/resources/js/app/locales.js:
--------------------------------------------------------------------------------
1 | import { addLocale, useLocale } from 'ttag';
2 |
3 | /**
4 | * Setup locale using locale parameter
5 | */
6 | export default function setupLocale(locale) {
7 | if (locale) {
8 | // Locale is the webpack alias that points to locales path
9 | const translationObj = require(`Locale/${locale}/default.po`);
10 | addLocale(locale, translationObj);
11 | useLocale(locale);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/templates/Element/Form/publish_properties.scss:
--------------------------------------------------------------------------------
1 | // TODO: refactor
2 | .fieldset#publish-properties {
3 | .tab-container {
4 | display: flex;
5 | flex-wrap: wrap;
6 |
7 | > div {
8 | width: calc(50% - 1em);
9 |
10 | &:first-child { width: 100%; }
11 |
12 | &.input.status {
13 | width: 100%;
14 | margin-bottom: 0;
15 | }
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/tests/files/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "app.css": "/css/app.css",
3 | "app.js": "/js/app.bundle.a34d91.js",
4 | "manifest.js": "/js/manifest.bundle.61b75d.js",
5 | "menu.js": "/js/menu.bundle.8efe63.js",
6 | "category.js": "/js/category.bundle.fb6024.js",
7 | "index-cell.js": "/js/index-cell.bundle.62f0cc.js",
8 | "vendors.css": "/css/vendors.css",
9 | "vendors.js": "/js/vendors.bundle.a0cb2d.js",
10 | "css/richeditor.lazy.scss": "/css/richeditor.lazy.css"
11 | }
12 |
--------------------------------------------------------------------------------
/resources/js/app/components/event-bus.vue:
--------------------------------------------------------------------------------
1 |
20 |
--------------------------------------------------------------------------------
/webroot/svg/iconLocked.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | ; This file is for unifying the coding style for different editors and IDEs.
2 | ; More information at http://editorconfig.org
3 |
4 | root = true
5 |
6 | [*]
7 | indent_style = space
8 | indent_size = 4
9 | end_of_line = lf
10 | insert_final_newline = true
11 | trim_trailing_whitespace = true
12 |
13 | [*.bat]
14 | end_of_line = crlf
15 |
16 | [*.yml]
17 | indent_style = space
18 | indent_size = 2
19 |
20 | [Vagrantfile]
21 | indent_style = space
22 | indent_size = 2
23 |
--------------------------------------------------------------------------------
/templates/Element/FilterBox/filter_box.twig:
--------------------------------------------------------------------------------
1 |
2 | {% if not hideFilter %}
3 |
4 | {{ element('FilterBox/filter_box_common', { showFilterSearchByType, hideFilterRelations }) }}
5 |
6 | {% endif %}
7 |
8 |
15 |
16 |
--------------------------------------------------------------------------------
/tests/.env.example:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # Used as a default to seed `tests/.env` which
3 | # enables you to use environment variables to configure
4 | # the test environment.
5 | #
6 | # To use this file, first copy it into `tests/.env`.
7 |
8 | # Uncomment these to define BEDITA API base URL and API KEY
9 | export BEDITA_API="https://bedita-api-url"
10 | export BEDITA_API_KEY="bedita-api-key"
11 |
12 | # Set admin credentials
13 | export BEDITA_ADMIN_USR="admin"
14 | export BEDITA_ADMIN_PWD="admin"
15 |
--------------------------------------------------------------------------------
/templates/Element/Panel/panel.twig:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ element('Panel/relations_add') }}
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/phpcs.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 | ./config
4 | ./src
5 | ./tests
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/webroot/svg/iconDraft.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/templates/Pages/Model/Categories/index.twig:
--------------------------------------------------------------------------------
1 | {% do _view.assign('title', 'Model ' ~ resourceType|humanize) %}
2 | {% do _view.assign('bodyViewClass', 'view-module view-model') %}
3 |
4 |
9 |
10 | {{ element('Modules/index_header') }}
11 |
12 |
13 |
14 | {{ element('Modules/index_categories', { showType: 1 }) }}
15 | {{ element('Model/sidebar_links') }}
16 |
17 |
{# end module-content #}
18 |
--------------------------------------------------------------------------------
/templates/Element/Form/categories.scss:
--------------------------------------------------------------------------------
1 | .categories-container {
2 | .categories {
3 | margin: $gutter 0;
4 |
5 | .select {
6 | columns: 2 auto;
7 | }
8 |
9 | h3 {
10 | font-weight: bold;
11 | text-transform: uppercase;
12 | margin: 0;
13 | }
14 |
15 | + .categories {
16 | margin-top: $gutter * 2;
17 |
18 | &:last-child {
19 | margin-bottom: $gutter * 4;
20 | }
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/templates/Element/Form/custom_left.twig:
--------------------------------------------------------------------------------
1 | {# Intentionally left blank: you may override this element in a plugin to create a custom view block
2 |
3 | To do so use a configuration like this in config/app.php:
4 |
5 | 'Elements' => [
6 | 'my_models' => [
7 | // element path => plugin to use
8 | 'Form/custom_left' => 'MyPlugin.Form/my_custom_template',
9 | ],
10 | ],
11 |
12 | This way for `my_models` object type a template file `MyPlugin.Form/my_custom_template` is loaded instead of this blank file #}
13 |
--------------------------------------------------------------------------------
/templates/Element/Form/custom_right.twig:
--------------------------------------------------------------------------------
1 | {# Intentionally left blank: you may override this element in a plugin to create a custom view block
2 |
3 | To do so use a configuration like this in config/app.php:
4 |
5 | 'Elements' => [
6 | 'my_models' => [
7 | // element path => plugin to use
8 | 'Form/custom_right' => 'MyPlugin.Form/my_custom_template',
9 | ],
10 | ],
11 |
12 | This way for `my_models` object type a template file `MyPlugin.Form/my_custom_template` is loaded instead of this blank file #}
13 |
--------------------------------------------------------------------------------
/templates/Pages/Admin/Appearance/index.twig:
--------------------------------------------------------------------------------
1 | {{ element('Admin/sidebar') }}
2 |
3 | {% do _view.assign('title', __('Administration') ~ ' ' ~ __('Appearance')) %}
4 | {% do _view.assign('bodyViewClass', 'view-module view-admin') %}
5 |
6 | {#
7 | // i18n
8 | __('Alert Message')
9 | __('Alert Message By Area')
10 | __('Export')
11 | __('Modules')
12 | __('Pagination')
13 | __('Project')
14 | __('Properties')
15 | #}
16 |
17 |
20 |
21 |
--------------------------------------------------------------------------------
/tests/Utils/ImportFilterSampleError.php:
--------------------------------------------------------------------------------
1 |
4 |
5 | {{ __('Setup module') }}
6 |
7 |
8 |
9 |
13 |
14 |
15 |
19 |
20 |
--------------------------------------------------------------------------------
/templates/Element/Form/title.twig:
--------------------------------------------------------------------------------
1 | {% if object.id %}
2 |
3 |
4 | {% for obj in included %}
5 | {% if obj.type == 'streams' %}
6 |
7 |
8 | {{ __('Open File') }}
9 |
10 | {% endif %}
11 | {% endfor %}
12 |
13 | {% else %}
14 | {{ __('New object in') }}
15 | {{ Layout.tr(object.type) }}
16 | {% endif %}
17 |
--------------------------------------------------------------------------------
/templates/Element/Form/calendar.scss:
--------------------------------------------------------------------------------
1 | .date-ranges-list {
2 | display: flex;
3 | flex-direction: column;
4 | margin-bottom: $gutter;
5 |
6 | .date-ranges-item {
7 | display: grid;
8 | grid-template-columns: 250px 250px 1fr 1fr 1fr;
9 | gap: $gutter;
10 | margin-bottom: $gutter * .5;
11 | align-items: flex-end;
12 |
13 | .weekdays {
14 | grid-column-start: 1;
15 | grid-column-gap: $gutter * .5;
16 | grid-column-end: 5;
17 |
18 | label {
19 | display: inline !important;
20 | }
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/templates/Pages/Modules/index.twig:
--------------------------------------------------------------------------------
1 | {% do _view.assign('title', __(currentModule.name|humanize)) %}
2 | {% set indexViewType = Layout.moduleIndexViewType() %}
3 |
4 | {{ element('Modules/index_header', { 'meta': meta, 'filter': filter, 'Schema': Schema, 'hidePagination': indexViewType == 'tree'}) }}
5 |
6 | {% set ids = Array.extract(objects, '{*}.id') %}
7 |
8 |
9 | {{ element('Modules/' ~ indexViewType) }}
10 | {{ element(Element.sidebar()) }}
11 |
12 |
13 |
--------------------------------------------------------------------------------
/templates/Pages/Admin/SystemInfo/index.twig:
--------------------------------------------------------------------------------
1 | {{ element('Admin/sidebar') }}
2 |
3 | {% do _view.assign('title', __('Administration') ~ ' ' ~ __('System information')) %}
4 | {% do _view.assign('bodyViewClass', 'view-module view-admin') %}
5 |
6 | BEdita Manager
7 |
8 |
9 |
10 |
11 | API
12 |
13 |
14 |
15 | {{ __('API could be distributed in multiple nodes. The following data refers to a single node') }}
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/templates/Element/Form/core_properties.twig:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 | {{ __('General') }}
7 |
8 |
9 | {{ element('Form/group_properties', {'properties' : properties.core, 'group': 'core'}) }}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/templates/Element/Form/publish_properties.twig:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 | {{ __('Publishing properties') }}
7 |
8 |
9 | {{ element('Form/group_properties', {'properties' : properties.publish, 'group': 'publish'}) }}
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/View/Helper/DatesHelper.php:
--------------------------------------------------------------------------------
1 | diff(new DateTime($then));
24 |
25 | return $diff->days;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/config/relations.php:
--------------------------------------------------------------------------------
1 | [
9 | 'children' => [
10 | 'type' => 'relations',
11 | 'attributes' => [
12 | 'name' => 'children',
13 | 'inverse_name' => 'children',
14 | 'label' => 'Folder children',
15 | 'inverse_label' => 'Folder children',
16 | 'params' => null,
17 | ],
18 | 'left' => ['folders'],
19 | 'right' => ['objects'],
20 | ],
21 | ],
22 | ];
23 |
--------------------------------------------------------------------------------
/templates/Pages/Categories/index.twig:
--------------------------------------------------------------------------------
1 | {% do _view.assign('bodyViewClass', 'view-module view-model') %}
2 |
3 |
8 |
9 | {{ element('Modules/index_header', {'resourceType': 'categories'}) }}
10 |
11 |
12 | {{ element('Modules/index_categories') }}
13 |
14 | {% do _view.append('app-module-buttons',
15 | Html.link(__('List'),
16 | {
17 | '_name': 'modules:list',
18 | 'object_type': objectType
19 | },
20 | {'class': 'button button-outlined'}
21 | )
22 | ) %}
23 |
24 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
5 |
6 | version: 2
7 | updates:
8 |
9 | - package-ecosystem: "github-actions"
10 | directory: "/"
11 | schedule:
12 | interval: "weekly"
13 | groups:
14 | gh-actions:
15 | patterns: ['actions/*']
16 | bedita:
17 | patterns: ['bedita/*']
18 | docker:
19 | patterns: ['docker/*']
20 |
--------------------------------------------------------------------------------
/templates/Element/Form/advanced_properties.twig:
--------------------------------------------------------------------------------
1 | {% if properties.advanced %}
2 |
3 |
4 |
5 |
8 | {{ __('Advanced') }}
9 |
10 |
11 |
12 |
13 | {{ element('Form/group_properties', {'properties' : properties.advanced, 'group': 'advanced'}) }}
14 |
15 |
16 |
17 |
18 |
19 | {% endif %}
20 |
--------------------------------------------------------------------------------
/index.php:
--------------------------------------------------------------------------------
1 | Snake Case
7 | *
8 | * @param {String} str the string to convert
9 | * @return {String}
10 | */
11 | Vue.filter('humanize', function(str) {
12 | return humanizeString(str);
13 | });
14 |
15 | /**
16 | * Capitalize a string.
17 | *
18 | * @param {String} str The string to capitalize
19 | */
20 | Vue.filter('capitalize', function(str) {
21 | if (!str) {
22 | return '';
23 | }
24 |
25 | str = str.toString();
26 | return str.charAt(0).toUpperCase() + str.slice(1);
27 | });
28 |
--------------------------------------------------------------------------------
/tests/Utils/ImportFilterSample.php:
--------------------------------------------------------------------------------
1 |
14 |
15 | {% endif %}
16 |
--------------------------------------------------------------------------------
/webroot/js/libs/timezone.js:
--------------------------------------------------------------------------------
1 | // timezone offset used at login
2 | Date.prototype.stdTimezoneOffset = function() {
3 | var jan = new Date(this.getFullYear(), 0, 1);
4 | var jul = new Date(this.getFullYear(), 6, 1);
5 | return Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset());
6 | }
7 |
8 | Date.prototype.dst = function() {
9 | return this.getTimezoneOffset() < this.stdTimezoneOffset();
10 | }
11 |
12 | document.addEventListener("DOMContentLoaded", function() {
13 | var inputTZElement = document.getElementById('timezone-offset');
14 | if (inputTZElement) {
15 | var tz = (-60) * (new Date().getTimezoneOffset()) + ' ' + (new Date().dst() ? '1' : '0');
16 | inputTZElement.value = tz;
17 | }
18 | });
19 |
--------------------------------------------------------------------------------
/config/schema/i18n.sql:
--------------------------------------------------------------------------------
1 | # Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
2 | #
3 | # Licensed under The MIT License
4 | # For full copyright and license information, please see the LICENSE.txt
5 | # Redistributions of files must retain the above copyright notice.
6 | # MIT License (https://opensource.org/licenses/mit-license.php)
7 |
8 | CREATE TABLE i18n (
9 | id int NOT NULL auto_increment,
10 | locale varchar(6) NOT NULL,
11 | model varchar(255) NOT NULL,
12 | foreign_key int(10) NOT NULL,
13 | field varchar(255) NOT NULL,
14 | content text,
15 | PRIMARY KEY (id),
16 | UNIQUE INDEX I18N_LOCALE_FIELD(locale, model, foreign_key, field),
17 | INDEX I18N_FIELD(model, foreign_key, field)
18 | );
19 |
--------------------------------------------------------------------------------
/templates/Layout/Email/text/default.ctp:
--------------------------------------------------------------------------------
1 | fetch('content');
18 |
--------------------------------------------------------------------------------
/templates/Element/Form/bulk_trash.twig:
--------------------------------------------------------------------------------
1 | {% if (objects) and Perms.canDelete({type: objectType}) %}
2 | {{ Form.create(null, {
3 | 'id': 'form-delete',
4 | 'url': {'_name': 'modules:delete', 'object_type': objectType}
5 | })|raw}}
6 |
7 | {% do Form.unlockField('ids') %}
8 |
9 |
10 |
15 |
16 | {{ __('Move to trash') }}
17 |
18 |
19 | {{ Form.end()|raw }}
20 | {% endif %}
21 |
--------------------------------------------------------------------------------
/templates/Email/text/default.ctp:
--------------------------------------------------------------------------------
1 | module.default)
10 | .then((component) => {
11 | const Constructor = Vue.extend(component);
12 | const vm = new Constructor({
13 | propsData: {
14 | el,
15 | }
16 | });
17 | vm.$mount();
18 | });
19 | },
20 | });
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/psalm.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/resources/js/app/directives/email.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Uri vue directive
3 | */
4 | export default {
5 | install(Vue) {
6 | Vue.directive('email', {
7 | inserted (el) {
8 | import(/* webpackChunkName: "email-input" */'app/components/email-input')
9 | .then(module => module.default)
10 | .then((component) => {
11 | const Constructor = Vue.extend(component);
12 | const vm = new Constructor({
13 | propsData: {
14 | el,
15 | }
16 | });
17 | vm.$mount();
18 | });
19 | },
20 | });
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/webroot/svg/iconFuture.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/resources/js/app/components/charts/bar-chart.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
32 |
--------------------------------------------------------------------------------
/templates/Pages/Model/Tags/index.twig:
--------------------------------------------------------------------------------
1 | {% do _view.assign('title', 'Model ' ~ resourceType|humanize) %}
2 | {% do _view.assign('bodyViewClass', 'view-module view-model') %}
3 |
4 | {% set ids = Array.extract(resources, '{*}.id') %}
5 | {% set _csrfToken = _view.request.params['_csrfToken']|default('')|json_encode %}
6 |
7 | {{ element('Modules/index_header', { 'hideFilter': 1 }) }}
8 |
9 |
14 |
15 |
16 |
17 | {{ element('Modules/index_tags') }}
18 | {% if not hideSidebar %}
19 | {{ element('Model/sidebar_links') }}
20 | {% endif %}
21 |
22 |
23 |
--------------------------------------------------------------------------------
/config/schema/sessions.sql:
--------------------------------------------------------------------------------
1 | # Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
2 | #
3 | # Licensed under The MIT License
4 | # For full copyright and license information, please see the LICENSE.txt
5 | # Redistributions of files must retain the above copyright notice.
6 | # MIT License (https://opensource.org/licenses/mit-license.php)
7 |
8 | CREATE TABLE `sessions` (
9 | `id` char(40) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
10 | `created` datetime DEFAULT CURRENT_TIMESTAMP, -- optional, requires MySQL 5.6.5+
11 | `modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- optional, requires MySQL 5.6.5+
12 | `data` blob DEFAULT NULL, -- for PostgreSQL use bytea instead of blob
13 | `expires` int(10) unsigned DEFAULT NULL,
14 | PRIMARY KEY (`id`)
15 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
16 |
--------------------------------------------------------------------------------
/templates/Email/html/default.ctp:
--------------------------------------------------------------------------------
1 | ' . $line . "\n";
21 | endforeach;
22 |
--------------------------------------------------------------------------------
/templates/Element/Panel/panel.scss:
--------------------------------------------------------------------------------
1 | .main-panel-container {
2 | position: fixed;
3 | top: 0; right: 0;
4 | min-width: 20 * $gutter;
5 | width: 100%;
6 | @media screen and (min-width: 768px) {
7 | width: 60%;
8 | }
9 | height: 100vh;
10 | transform: translateX(101%);
11 | transition: .3s transform;
12 | will-change: transform;
13 | z-index: 1001;
14 |
15 | .main-panel {
16 | overflow-x: hidden;
17 | overflow-y: auto;
18 | height: 100%;
19 | background-color: $gray-800;
20 | }
21 |
22 | .panel-slot {
23 | height: 100%;
24 | }
25 |
26 | &.on {
27 | transform: translateX(0);
28 | box-shadow: 0 0 32px $black;
29 | }
30 | }
31 |
32 | .main-panel {
33 | .fieldset:first-child {
34 | padding-top: $gutter;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/templates/Pages/Import/index.twig:
--------------------------------------------------------------------------------
1 | {% do _view.assign('title', __('Data Import')) %}
2 |
3 | {% if 'POST' in jobsAllow %}
4 | {% if filters %}
5 | {{ Form.create(null, { 'id': 'form-import', 'type': 'file', 'url': {'_name': 'import:file'} })|raw }}
6 |
7 |
8 | {{ Form.hidden('MAX_FILE_SIZE', { 'value': System.getMaxFileSize() })|raw }}
9 | {{ Form.end()|raw }}
10 | {% else %}
11 |
12 | {{ __('No import filters set') }}
13 |
14 | {% endif %}
15 |
16 |
17 |
18 | {% endif %}
19 |
20 | {% if 'GET' in jobsAllow %}
21 |
22 |
23 | {% endif %}
24 |
--------------------------------------------------------------------------------
/src/Controller/Admin/UserAccessesController.php:
--------------------------------------------------------------------------------
1 | for more details.
12 | */
13 | namespace App\Controller\Admin;
14 |
15 | use Cake\Http\Response;
16 |
17 | /**
18 | * User Accesses Controller
19 | */
20 | class UserAccessesController extends AdministrationBaseController
21 | {
22 | /**
23 | * @inheritDoc
24 | */
25 | public function index(): ?Response
26 | {
27 | return null;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/resources/js/app/components/form/field-title.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 | {{ getTitle() }}
7 | *
8 |
9 |
10 |
34 |
39 |
--------------------------------------------------------------------------------
/templates/Element/Model/relation_types.twig:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{__('Left types') }}
4 |
5 | {% do Form.unlockField('current_left') %}
6 | {% do Form.unlockField('change_left') %}
7 |
8 |
9 |
{{__('Right types') }}
10 |
11 | {% do Form.unlockField('current_right') %}
12 | {% do Form.unlockField('change_right') %}
13 |
14 |
15 |
--------------------------------------------------------------------------------
/webroot/svg/iconExpired.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/resources/js/app/components/form/field-string.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
40 |
--------------------------------------------------------------------------------
/templates/Layout/Email/html/default.ctp:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 | = $this->fetch('title') ?>
21 |
22 |
23 | = $this->fetch('content') ?>
24 |
25 |
26 |
--------------------------------------------------------------------------------
/resources/js/app/components/horizontal-tab-view.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Templates that uses this component (directly or indirectly):
3 | * ...
4 | *
5 | * component used for ModulesPage -> View
6 | *
7 | * Handle horizontal tabs
8 | *
9 | * @prop {Array} labels
10 | * @prop {int} defaultActive index of the default active label in labels
11 | *
12 | */
13 | export default {
14 | components: {
15 | FormFileUpload: () => import(/* webpackChunkName: "form-file-upload" */'app/components/form-file-upload'),
16 | },
17 |
18 | props: {
19 | labels: {
20 | type: Array,
21 | default: [],
22 | },
23 | defaultActive: {
24 | type: Number,
25 | default: 0,
26 | },
27 | },
28 |
29 | data() {
30 | return {
31 | activeIndex: this.defaultActive,
32 | }
33 | },
34 |
35 | methods: {
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/resources/js/app/components/form/field-plaintext.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
40 |
--------------------------------------------------------------------------------
/templates/Pages/Password/change.twig:
--------------------------------------------------------------------------------
1 |
2 |
{{ __('Change password') }}
3 |
{{ __('You have requested a password change, please provide a new password') }}.
4 |
5 |
6 | {{ Form.create(null, {'url': {'_name': 'password:change'}})|raw }}
7 |
8 | {{ Form.control('uuid', {'type': 'hidden', 'value': uuid})|raw }}
9 |
10 |
11 |
{{ __('New password') }}
12 |
13 | {{ Form.password('password', {'placeholder': '••••••'})|raw }}
14 |
15 |
16 |
17 |
18 |
{{ __('Confirm new password') }}
19 |
20 | {{ Form.password('checkpassword', {'placeholder': '••••••'})|raw }}
21 |
22 |
23 |
24 |
25 | {{ Form.button(__('Change password'), {'type': 'submit'})|raw }}
26 |
27 |
28 | {{ Form.end()|raw }}
29 |
--------------------------------------------------------------------------------
/resources/styles/_non-production.scss:
--------------------------------------------------------------------------------
1 | // non production environemnt
2 | .cake-error {
3 | position: relative;
4 | z-index: 1;
5 | max-width: 100%;
6 | padding: 1rem;
7 | margin: .5rem;
8 | overflow-x: auto;
9 | white-space: pre-wrap;
10 | word-wrap: break-word;
11 | background: #000;
12 | color: $gray-300;
13 | font-size: 12px;
14 |
15 | a {
16 | color: #62b9f3;
17 | }
18 | }
19 |
20 | body[alert-message] {
21 | padding-top: 2rem !important;
22 |
23 | &:after {
24 | content: attr(alert-message);
25 | position: fixed;
26 | top: 0;
27 | left: 0;
28 | height: 2rem;
29 | background-color: crimson;
30 | width: 100%;
31 | display: flex;
32 | justify-content: center;
33 | align-items: center;
34 | z-index: 2;
35 | }
36 |
37 | main.layout > .layout-sidebar > .sidebar {
38 | top: 2rem;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Form/CustomHandlerInterface.php:
--------------------------------------------------------------------------------
1 | for more details.
12 | */
13 |
14 | namespace App\Form;
15 |
16 | interface CustomHandlerInterface
17 | {
18 | /**
19 | * Create custom form control options for a property
20 | *
21 | * @param string $name Property name
22 | * @param mixed $value Property value
23 | * @param array $options Options array from configuration
24 | * @return array
25 | */
26 | public function control(string $name, $value, array $options): array;
27 | }
28 |
--------------------------------------------------------------------------------
/bin/cake.bat:
--------------------------------------------------------------------------------
1 | ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
2 | ::
3 | :: Cake is a Windows batch script for invoking CakePHP shell commands
4 | ::
5 | :: CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
6 | :: Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
7 | ::
8 | :: Licensed under The MIT License
9 | :: Redistributions of files must retain the above copyright notice.
10 | ::
11 | :: @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
12 | :: @link https://cakephp.org CakePHP(tm) Project
13 | :: @since 2.0.0
14 | :: @license https://opensource.org/licenses/mit-license.php MIT License
15 | ::
16 | ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
17 |
18 | @echo off
19 |
20 | SET app=%0
21 | SET lib=%~dp0
22 |
23 | php "%lib%cake.php" %*
24 |
25 | echo.
26 |
27 | exit /B %ERRORLEVEL%
28 |
--------------------------------------------------------------------------------
/src/Core/Bulk/CustomBulkActionInterface.php:
--------------------------------------------------------------------------------
1 | for more details.
12 | */
13 |
14 | namespace App\Core\Bulk;
15 |
16 | /**
17 | * Custom bulk action interface
18 | */
19 | interface CustomBulkActionInterface
20 | {
21 | /**
22 | * Perform bulk action.
23 | *
24 | * @param array $ids Object IDs
25 | * @param string $type Object type
26 | * @return array Error details on failure, empty array on success
27 | */
28 | public function bulkAction(array $ids, string $type): array;
29 | }
30 |
--------------------------------------------------------------------------------
/templates/Element/Form/bulk_category.twig:
--------------------------------------------------------------------------------
1 | {% if schema.categories|length > 0 %}
2 | {{ Form.create(null, {
3 | 'id': 'bulk-categories',
4 | 'url': {'_name': 'modules:bulkCategories', 'object_type': objectType, '?': _view.request.getQuery()},
5 | })|raw }}
6 |
7 |
8 | {% do Form.unlockField('categories') %}
9 |
10 |
11 | {% do Form.unlockField('ids') %}
12 |
13 |
14 |
15 | {{ __('Ok') }}
16 |
17 |
18 | {{ Form.end()|raw }}
19 | {% endif %}
20 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ./src/
6 |
7 |
8 | ./src/Console/Installer.php
9 | ./src/Shell/ConsoleShell.php
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | ./tests/TestCase
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/resources/js/app/components/form/field-integer.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
40 |
45 |
--------------------------------------------------------------------------------
/src/Controller/Admin/ObjectsHistoryController.php:
--------------------------------------------------------------------------------
1 | for more details.
12 | */
13 | namespace App\Controller\Admin;
14 |
15 | /**
16 | * Objects History Controller
17 | */
18 | class ObjectsHistoryController extends AdministrationBaseController
19 | {
20 | /**
21 | * @inheritDoc
22 | */
23 | protected $resourceType = 'applications';
24 |
25 | /**
26 | * @inheritDoc
27 | */
28 | protected $readonly = true;
29 |
30 | /**
31 | * @inheritDoc
32 | */
33 | protected $properties = [
34 | 'name' => 'string',
35 | ];
36 | }
37 |
--------------------------------------------------------------------------------
/resources/js/app/components/email-input.js:
--------------------------------------------------------------------------------
1 | import { t } from 'ttag';
2 |
3 | export default {
4 | template: /* template */`
5 |
6 |
7 |
8 | `,
9 |
10 | props: {
11 | el: {
12 | type: HTMLInputElement,
13 | },
14 | isValid: false
15 | },
16 |
17 | async mounted() {
18 | if (this.el.value === 'null') {
19 | this.el.value = '';
20 | this.isValid = true;
21 | }
22 | const span = document.createElement('span');
23 | span.title = t`Mail to`;
24 | span.classList.add('icon-mail-1');
25 | span.style = 'cursor: pointer;';
26 | span.addEventListener('click', (ev) => {
27 | ev.preventDefault()
28 | ev.stopPropagation();
29 | if (this.el.value.length < 8) {
30 | return;
31 | }
32 | window.open(`mailto:${this.el.value}`);
33 | });
34 | this.el.parentElement.appendChild(span);
35 | },
36 | };
37 |
--------------------------------------------------------------------------------
/resources/js/app/components/form/field-number.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
41 |
46 |
--------------------------------------------------------------------------------
/resources/js/app/directives/jsoneditor.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * v-jsoneditor directive to activate jsoneditor on element
4 | *
5 | */
6 |
7 | export default {
8 | install(Vue) {
9 | Vue.directive('jsoneditor', {
10 | /**
11 | * dynamic load json-editor-input component and mount it
12 | *
13 | * @param {Object} element DOM object
14 | */
15 | inserted (el) {
16 | import(/* webpackChunkName: "json-editor-input" */'app/components/json-editor-input')
17 | .then(module => module.default)
18 | .then((component) => {
19 | const Constructor = Vue.extend(component);
20 | const vm = new Constructor({
21 | propsData: {
22 | el,
23 | }
24 | });
25 | vm.$mount();
26 | });
27 | },
28 | })
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/bin/perms.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ################################################################################
4 | # Shell script to update permissions on a BE4 webapp
5 | # Shell user and web server have rwx permissions on tmp/ and logs/
6 | ################################################################################
7 |
8 | HTTPDUSER=`ps aux | grep -E '[a]pache|[h]ttpd|[_]www|[w]ww-data|[n]ginx' | grep -v root | head -1 | cut -d\ -f1`
9 |
10 | if [ -z "$HTTPDUSER" ]; then
11 | echo "Web server user not found, verify that a webserver service (like Apache2) is up & running"
12 | exit 1;
13 | fi
14 |
15 | echo "Web server user is: $HTTPDUSER"
16 |
17 | echo "setfacl -R -m u:${HTTPDUSER}:rwx tmp"
18 | setfacl -R -m u:${HTTPDUSER}:rwx tmp
19 |
20 | echo "setfacl -R -d -m u:${HTTPDUSER}:rwx tmp"
21 | setfacl -R -d -m u:${HTTPDUSER}:rwx tmp
22 |
23 | echo "setfacl -R -m u:${HTTPDUSER}:rwx logs"
24 | setfacl -R -m u:${HTTPDUSER}:rwx logs
25 |
26 | echo "setfacl -R -d -m u:${HTTPDUSER}:rwx logs"
27 | setfacl -R -d -m u:${HTTPDUSER}:rwx logs
28 |
--------------------------------------------------------------------------------
/templates/Element/Form/permissions.twig:
--------------------------------------------------------------------------------
1 | {% if object.meta.perms is defined %}
2 |
3 |
4 |
7 | {{ __('Permissions') }}
8 |
9 |
10 | {% set objectPerms = object.meta.perms|default({})|json_encode %}
11 | {% if objectPerms == '[]' %}{% set objectPerms = '{}' %}{% endif %}
12 |
13 | {% set readonlyRoles = config('Permissions.readonly')|default([]) %}
14 |
15 |
22 |
23 |
24 | {% endif %}
25 |
--------------------------------------------------------------------------------
/src/Controller/CourtesyPageController.php:
--------------------------------------------------------------------------------
1 | for more details.
12 | */
13 | namespace App\Controller;
14 |
15 | use Cake\Controller\Controller;
16 | use Cake\Core\Configure;
17 | use Cake\Http\Response;
18 |
19 | /**
20 | * CourtesyPage Controller
21 | */
22 | class CourtesyPageController extends Controller
23 | {
24 | /**
25 | * Index method
26 | *
27 | * @return \Cake\Http\Response|null
28 | */
29 | public function index(): ?Response
30 | {
31 | $message = Configure::read('Maintenance.message');
32 | $this->set(compact('message'));
33 |
34 | return null;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/webroot/svg/concurrent-editors.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/templates/Element/Modules/list.twig:
--------------------------------------------------------------------------------
1 |
2 |
3 | {% if objects %}
4 | {{ element('Modules/index_table_header', { 'refObject': objects[0] }) }}
5 | {% endif %}
6 |
7 | {% for object in objects %}
8 | {{ element('Modules/index_table_row', { 'object': object }) }}
9 | {% else %}
10 | {{ __('No items found') }}
11 | {% endfor %}
12 |
13 |
14 |
15 |
30 |
--------------------------------------------------------------------------------
/resources/js/app/components/thumbnail/thumbnail.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
35 |
--------------------------------------------------------------------------------
/templates/Element/Modules/index_properties.scss:
--------------------------------------------------------------------------------
1 | .index-date-ranges {
2 | display: flex;
3 |
4 | // toggle button
5 | .show-toggle {
6 | margin-left: $gutter;
7 | line-height: 1;
8 | height: 20px;
9 | cursor: cell;
10 | &:focus {
11 | outline: none;
12 | }
13 | }
14 |
15 | // default show only first child
16 | .date-range:not(:first-child) { display: none; }
17 | &.show-all {
18 | .date-range:not(:first-child) { display: flex; }
19 | }
20 |
21 | .date-range {
22 | display: flex;
23 | .date-range ~ .date-range {
24 | margin-top: $gutter * .25;
25 | }
26 | &:last-child:not(:first-child) { margin-bottom: 1px; } // adjust space when expanded
27 |
28 | .date-item {
29 | display: flex;
30 | &:first-child {
31 | margin-right: $gutter * .5;
32 | }
33 |
34 | .date {
35 | margin-left: $gutter * .5;
36 | font-weight: $font-weight-bold;
37 | }
38 | }
39 |
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/templates/Error/error400.twig:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ __('Error ') }} {{ error.code }}
4 |
5 |
6 |
7 |
8 | {{ message }}
9 |
10 |
11 | {% if config('debug') %}
12 |
13 | {% if error %}
14 |
15 |
16 | Error in:
17 | {{ error.getFile }}, line {{ error.getLine }}
18 |
19 | {% endif %}
20 |
21 | {% endif %}
22 |
23 |
24 |
30 |
31 |
32 |
33 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/templates/Error/error500.twig:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ __('Error ') }} {{ error.code }}
4 |
5 |
6 |
7 |
8 | {{ message }}
9 |
10 |
11 | {% if config('debug') %}
12 |
13 | {% if error %}
14 |
15 |
16 | Error in:
17 | {{ error.getFile }}, line {{ error.getLine }}
18 |
19 | {% endif %}
20 |
21 | {% endif %}
22 |
23 |
24 |
30 |
31 |
32 |
33 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/resources/js/app/components/form/field-checkbox.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
44 |
50 |
--------------------------------------------------------------------------------
/templates/Element/Form/multiupload.twig:
--------------------------------------------------------------------------------
1 | {% do _view.assign('title', __('Multi upload')) %}
2 | {% if Perms.canSave() and not readonly %}
3 |
4 |
5 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | {% else %}
18 | {{ __('You do not have the required permissions to view this page.') }}
19 | {% endif %}
20 |
21 | {# Add links to the module #}
22 | {% do _view.append('app-module-links',
23 | Html.link(
24 | __('List'),
25 | {'_name': 'modules:list', 'object_type': objectType},
26 | {'title': __('List'), 'class': 'icon-left-dir button button-outlined button-outlined-hover-module-' ~ objectType}
27 | )|raw
28 | ) %}
29 |
30 |
--------------------------------------------------------------------------------
/templates/Element/Form/history.twig:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ __('History') }}
6 |
7 | <: totalObjects :>
8 |
9 |
10 |
11 |
12 |
13 |
14 | {% block info %}{% endblock %}
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/resources/js/app/pages/admin/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Templates that uses this component (directly or indirectly)
3 | * Template/Pages/Admin/../*.twig
4 | *
5 | * component used for Admin index pages
6 | */
7 |
8 | import { confirm } from 'app/components/dialog/dialog';
9 | import { t } from 'ttag';
10 |
11 | export default {
12 | components: {
13 | PropertyView: () => import(/* webpackChunkName: "property-view" */'app/components/property-view/property-view'),
14 | Secret: () => import(/* webpackChunkName: "secret" */'app/components/secret/secret'),
15 | ShowHide:() => import(/* webpackChunkName: "show-hide" */'app/components/show-hide/show-hide'),
16 | },
17 | data() {
18 | return {
19 | tabsOpen: true,
20 | };
21 | },
22 | methods: {
23 | remove(e) {
24 | const message = t`Remove item. Are you sure?`;
25 | const formId = e.target.closest('button').getAttribute('form');
26 | const form = document.getElementById(formId);
27 | confirm(message, t`yes, proceed`, form.submit.bind(form));
28 | },
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/templates/Pages/Admin/_admin.scss:
--------------------------------------------------------------------------------
1 | body.view-admin {
2 | .filter-box-admin > .filters-container > .filter-container > span {
3 | margin-left: 15px;
4 | }
5 | .has-background-module-admin {
6 | background-color: white !important;
7 | }
8 | .active-action {
9 | font-weight: bold;
10 | color: white;
11 | }
12 | .table-row {
13 | &.new-record > * {
14 | padding-top: calc($gutter * 4) !important;
15 | }
16 | }
17 | }
18 |
19 | .app-module-links {
20 | a:not(.button), span {
21 | border-bottom: 1px solid $gray-600;
22 | padding: calc($gutter * 0.5);
23 | padding-left: $gutter;
24 | margin: 0 !important;
25 | }
26 | .button {
27 | margin-top: calc($gutter * 2);
28 | }
29 | .active-action {
30 | background-color: $gray-600;
31 | font-weight: normal !important;
32 | color: black !important;
33 | }
34 | }
35 |
36 | .content {
37 | min-width: 400px;
38 | }
39 |
40 | .perm-hidden {
41 | opacity: .5;
42 | }
43 |
44 | .view-admin .module-index .fieldset .tab {
45 | gap: 5px;
46 | }
47 |
--------------------------------------------------------------------------------
/templates/Element/Form/tags.twig:
--------------------------------------------------------------------------------
1 | {% if 'Tags' in schema.associations %}
2 | {% set objectTags = object.attributes.tags|default([]) %}
3 |
4 |
5 |
8 | {{ __('Tags') }}
9 | {{ objectTags|length }}
10 |
11 |
20 |
21 |
22 | {% endif %}
23 |
--------------------------------------------------------------------------------
/src/Utility/System.php:
--------------------------------------------------------------------------------
1 | for more details.
14 | */
15 |
16 | namespace App\Utility;
17 |
18 | class System
19 | {
20 | /**
21 | * Compare BEdita API version
22 | *
23 | * @param string $actual Actual API version
24 | * @param string $expected Expected API version
25 | * @return bool
26 | */
27 | public static function compareBEditaApiVersion(string $actual, string $expected): bool
28 | {
29 | $apiMajor = substr($actual, 0, strpos($actual, '.'));
30 | $requiredApiMajor = substr($expected, 0, strpos($expected, '.'));
31 |
32 | return $apiMajor === $requiredApiMajor && version_compare($actual, $expected) >= 0;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/resources/js/app/components/form/field-date.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
15 |
16 |
48 |
--------------------------------------------------------------------------------
/templates/Element/Form/group_properties.twig:
--------------------------------------------------------------------------------
1 | {% set customElement = Element.custom(group, 'group') %}
2 | {% if customElement %}
3 | {{ element(customElement, {'properties' : properties}) }}
4 | {% else %}
5 | {% set locked = object.meta.locked or (object.id and Perms.isLockedByParents(object.id)) %}
6 | {% for key, value in properties %}
7 | {% if locked and key == 'uname' %}
8 | {{ Property.control(key, value, {'readonly': 'readonly'})|raw }}
9 | {% elseif locked and key == 'status' %}
10 | {{ Property.control(key, value, {'disabled': 'disabled'})|raw }}
11 |
12 | {% elseif key in object.meta|keys %}
13 | {{ Property.control(key, value, {'disabled': true, 'readonly': true})|raw }}
14 | {% else %}
15 | {{ Property.control(key, value)|raw }}
16 | {% if schema.properties[key].placeholders %}
17 |
18 | {% endif %}
19 | {% endif %}
20 | {% endfor %}
21 | {% endif %}
22 |
--------------------------------------------------------------------------------
/templates/Element/Admin/sidebar.twig:
--------------------------------------------------------------------------------
1 | {# Append urls to sidebar #}
2 | {% set actions = [
3 | 'appearance',
4 | 'applications',
5 | 'auth_providers',
6 | 'endpoints',
7 | 'endpoint_permissions',
8 | 'external_auth',
9 | 'roles',
10 | 'roles_modules',
11 | 'config',
12 | 'async_jobs',
13 | 'objects_history',
14 | 'user_accesses',
15 | 'system_info',
16 | 'statistics',
17 | ] %}
18 | {% set controllerAction = _view.request.getparam('controller')|default('') %}
19 | {% for action in actions %}
20 | {% if action == controllerAction|underscore %}
21 | {% do _view.append('app-module-links', '' ~ __(action|underscore|humanize) ~ ' ') %}
22 | {% else %}
23 | {% do _view.append('app-module-links', Html.link(__(action|underscore|humanize), {'_name': 'admin:list:' ~ action}, {})|raw) %}
24 | {% endif %}
25 | {% endfor %}
26 |
27 | {% do _view.append(
28 | 'app-module-buttons',
29 | '' ~ __('Clear cache') ~ ' '
30 | ) %}
31 |
--------------------------------------------------------------------------------
/tests/TestCase/Utility/ApiClientTraitTest.php:
--------------------------------------------------------------------------------
1 | for more details.
14 | */
15 | namespace App\Test\TestCase\Utility;
16 |
17 | use App\Utility\ApiClientTrait;
18 | use Cake\TestSuite\TestCase;
19 |
20 | /**
21 | * {@see \App\Utility\ApiClientTrait} Test Case
22 | *
23 | * @coversDefaultClass \App\Utility\ApiClientTrait
24 | */
25 | class ApiClientTraitTest extends TestCase
26 | {
27 | use ApiClientTrait;
28 |
29 | /**
30 | * Test getClient method.
31 | *
32 | * @return void
33 | * @covers ::getClient()
34 | */
35 | public function testGetClient(): void
36 | {
37 | $this->apiClient = null;
38 | $actual = $this->getClient();
39 | static::assertNotNull($actual);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/tests/TestCase/PluginTest.php:
--------------------------------------------------------------------------------
1 | for more details.
12 | */
13 | namespace App\Test\TestCase;
14 |
15 | use App\Plugin;
16 | use Cake\Core\Configure;
17 | use Cake\TestSuite\TestCase;
18 |
19 | /**
20 | * \App\Plugin Test Case
21 | *
22 | * @coversDefaultClass \App\Plugin
23 | */
24 | class PluginTest extends TestCase
25 | {
26 | /**
27 | * Test loaded app plugins
28 | *
29 | * @return void
30 | * @covers ::loadedAppPlugins()
31 | */
32 | public function testLoadedAppPlugins(): void
33 | {
34 | $expected = Configure::read('Plugins', []);
35 | $expected = array_keys($expected);
36 | sort($expected);
37 | $loaded = Plugin::loadedAppPlugins();
38 | static::assertEquals($expected, $loaded);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/tests/TestCase/Controller/CourtesyPageControllerTest.php:
--------------------------------------------------------------------------------
1 | $expected]);
27 | $config = [
28 | 'environment' => [
29 | 'REQUEST_METHOD' => 'GET',
30 | ],
31 | ];
32 | $request = new ServerRequest($config);
33 | $controller = new CourtesyPageController($request);
34 | $controller->index();
35 | $actual = $controller->viewBuilder()->getVar('message');
36 | static::assertEquals($expected, $actual);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/Utility/Schema.php:
--------------------------------------------------------------------------------
1 | for more details.
12 | */
13 |
14 | namespace App\Utility;
15 |
16 | use Cake\Utility\Hash;
17 |
18 | /**
19 | * Utility class to get schema data
20 | */
21 | class Schema
22 | {
23 | /**
24 | * Return unique alphabetically ordered right types from schema $schema
25 | *
26 | * @param array $schema Schema data.
27 | * @return array
28 | */
29 | public static function rightTypes(array $schema): array
30 | {
31 | $relationsRightTypes = (array)Hash::extract($schema, '{s}.right');
32 | $types = [];
33 | foreach ($relationsRightTypes as $rightTypes) {
34 | $types = array_unique(array_merge($types, $rightTypes));
35 | }
36 | sort($types);
37 |
38 | return $types;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/templates/Element/Form/meta.twig:
--------------------------------------------------------------------------------
1 | {% set meta = (object) ? object.meta : resource.meta %}
2 |
3 | {% if meta %}
4 |
5 |
6 |
7 |
10 | {{ __('Metadata') }}
11 |
12 |
13 |
14 |
15 |
16 |
17 | {{ __('id') }}:
18 | {{ (object) ? object.id : resource.id }}
19 |
20 | {% for key, val in meta %}
21 |
22 | {{ key }}:
23 | {{ Schema.format(val, schema.properties[key]) }}
24 |
25 | {% endfor %}
26 |
27 |
28 |
29 |
30 |
31 |
32 | {% endif %}
33 |
--------------------------------------------------------------------------------
/src/View/Helper/EditorsHelper.php:
--------------------------------------------------------------------------------
1 | for more details.
12 | */
13 | namespace App\View\Helper;
14 |
15 | use Cake\Cache\Cache;
16 | use Cake\Utility\Hash;
17 | use Cake\View\Helper;
18 |
19 | /**
20 | * Helper for editors
21 | */
22 | class EditorsHelper extends Helper
23 | {
24 | /**
25 | * Get list of object editors.
26 | *
27 | * @return array
28 | */
29 | public function list(): array
30 | {
31 | $id = (string)Hash::get((array)$this->getView()->get('object'), 'id');
32 | $cached = (string)Cache::read('objects_editors', 'default');
33 | $objectsEditors = (array)json_decode($cached, true);
34 | if (array_key_exists($id, $objectsEditors)) {
35 | return $objectsEditors[$id];
36 | }
37 |
38 | return [];
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Controller/Admin/EndpointsController.php:
--------------------------------------------------------------------------------
1 | for more details.
12 | */
13 | namespace App\Controller\Admin;
14 |
15 | /**
16 | * Endpoints Controller
17 | *
18 | * @property \App\Controller\Component\PropertiesComponent $Properties
19 | */
20 | class EndpointsController extends AdministrationBaseController
21 | {
22 | /**
23 | * Resource type in use
24 | *
25 | * @var string
26 | */
27 | protected $resourceType = 'endpoints';
28 |
29 | /**
30 | * @inheritDoc
31 | */
32 | protected $readonly = false;
33 |
34 | /**
35 | * @inheritDoc
36 | */
37 | protected $properties = [
38 | 'name' => 'string',
39 | 'description' => 'text',
40 | ];
41 |
42 | /**
43 | * @inheritDoc
44 | */
45 | protected $sortBy = 'name';
46 | }
47 |
--------------------------------------------------------------------------------
/src/Utility/ApiClientTrait.php:
--------------------------------------------------------------------------------
1 | for more details.
14 | */
15 | namespace App\Utility;
16 |
17 | use BEdita\SDK\BEditaClient;
18 | use BEdita\WebTools\ApiClientProvider;
19 |
20 | /**
21 | * Read and write configuration via API
22 | */
23 | trait ApiClientTrait
24 | {
25 | /**
26 | * BEdita Api client
27 | *
28 | * @var \BEdita\SDK\BEditaClient|null
29 | */
30 | protected $apiClient = null;
31 |
32 | /**
33 | * Get API client.
34 | *
35 | * @return \BEdita\SDK\BEditaClient
36 | */
37 | public function getClient(): BEditaClient
38 | {
39 | if ($this->apiClient === null) {
40 | $this->apiClient = ApiClientProvider::getApiClient();
41 | }
42 |
43 | return $this->apiClient;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/templates/Element/Form/other_properties.twig:
--------------------------------------------------------------------------------
1 | {# Remaining properties groups: 'other' and custom groups not in 'core', '_keep', 'publish', 'advanced', 'media' #}
2 | {% set otherProperties = Array.removeKeys(properties, ['core', '_keep', 'publish', 'advanced', 'media']) %}
3 | {% for group, props in otherProperties %}
4 |
5 | {% set customElement = Element.custom(group, 'group') %}
6 | {% if props or customElement %}
7 |
8 |
9 |
10 |
11 |
14 | {{ Layout.tr(group) }}
15 |
16 |
17 |
18 |
19 | {% if customElement %}
20 | {{ element(customElement) }}
21 | {% else %}
22 | {{ element('Form/group_properties', {'properties' : props, 'group': group}) }}
23 | {% endif %}
24 |
25 |
26 |
27 |
28 |
29 |
30 | {% endif %}
31 | {% endfor %}
32 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 | parse()
25 | ->putenv()
26 | ->toEnv()
27 | ->toServer();
28 | }
29 |
30 | \Cake\Cache\Cache::disable();
31 |
32 | if (empty(\Cake\Core\Configure::read('API'))) {
33 | \Cake\Core\Configure::write('API', [
34 | 'apiBaseUrl' => env('BEDITA_API'),
35 | 'apiKey' => env('BEDITA_API_KEY'),
36 | ]);
37 | }
38 |
39 | $_SERVER['PHP_SELF'] = '/';
40 |
41 | // ensure default locale is set to English
42 | Configure::write('I18n.default', 'en');
43 | Configure::write('I18n.lang', 'en');
44 | I18n::setLocale('en_US');
45 |
--------------------------------------------------------------------------------
/.github/workflows/javascript.yml:
--------------------------------------------------------------------------------
1 | name: javascript
2 |
3 | on:
4 | pull_request:
5 | paths:
6 | - '**/*.vue'
7 | - '**/*.js'
8 | - '**/*.scss'
9 | - '.github/workflows/javascript.yml'
10 | - 'package.json'
11 | - 'yarn.lock'
12 | push:
13 | paths:
14 | - '**/*.vue'
15 | - '**/*.js'
16 | - '**/*.scss'
17 | - '.github/workflows/javascript.yml'
18 | - 'package.json'
19 | - 'yarn.lock'
20 |
21 | jobs:
22 | javascript-build:
23 | name: Check javascript build
24 | if: "!contains(github.event.commits[0].message, '[skip ci]') && !contains(github.event.commits[0].message, '[ci skip]')"
25 | runs-on: ubuntu-latest
26 |
27 | strategy:
28 | matrix:
29 | node-version: [20.x, 22.x, 24.x]
30 |
31 | steps:
32 | - name: Checkout current revision
33 | uses: actions/checkout@v6
34 |
35 | - name: Use Node.js ${{ matrix.node-version }}
36 | uses: actions/setup-node@v6
37 | with:
38 | node-version: ${{ matrix.node-version }}
39 |
40 | - name: Yarn install
41 | run: yarn install
42 |
43 | - name: Yarn build
44 | run: yarn build
45 |
46 | - name: Run ESLint
47 | run: yarn eslint resources/js/**/*.js resources/js/**/*.vue
48 |
--------------------------------------------------------------------------------
/resources/js/app/components/autosize-textarea.js:
--------------------------------------------------------------------------------
1 | import autosize from 'autosize';
2 |
3 | export default {
4 | props: ['value', 'reset-value'],
5 |
6 | template: '',
7 |
8 | data() {
9 | return {
10 | text: '',
11 | originalValue: '',
12 | };
13 | },
14 |
15 | watch: {
16 | text() {
17 | // make sure render reactively
18 | this.$nextTick(() => {
19 | autosize(this.$el);
20 | });
21 | },
22 |
23 | value() {
24 | this.originalValue = this.value;
25 | }
26 | },
27 |
28 | mounted() {
29 | // setup initial value
30 | this.originalValue = this.value;
31 | this.text = this.value;
32 |
33 | // setup autosize
34 | this.$nextTick(() => {
35 | autosize(this.$el);
36 | });
37 | },
38 |
39 | methods: {
40 | /**
41 | *
42 | * @emits Event#input textarea value
43 | *
44 | * @param {Event} event event object
45 | */
46 | handleChange(event) {
47 | this.text = event.target.value;
48 | this.$emit('input', this.text);
49 | }
50 | }
51 | };
52 |
--------------------------------------------------------------------------------
/resources/js/libs/bedita.js:
--------------------------------------------------------------------------------
1 |
2 | import Vue from 'vue';
3 |
4 | /**
5 | * BEdita Helper Object
6 | */
7 | const BELoader = {
8 |
9 | /**
10 | * load Beplugins' Vue components (global)
11 | *
12 | * @return {void}
13 | */
14 | loadBeditaPlugins() {
15 | const plugins = BEDITA.plugins;
16 |
17 | plugins.forEach(element => {
18 | const vueComponent = window[element] || global[element];
19 | if (!vueComponent || !vueComponent.default) {
20 | return;
21 | }
22 | const BEPlugins = vueComponent.default;
23 |
24 | Object.keys(BEPlugins).forEach(componentName => {
25 | if (typeof BEPlugins[componentName] === 'object') {
26 | Vue.component(componentName, BEPlugins[componentName]);
27 |
28 | console.debug(
29 | `%c[${componentName}]%c component succesfully registred from %c${element}%c Plugin`,
30 | 'color: blue',
31 | 'color: black',
32 | 'color: red',
33 | 'color: black'
34 | );
35 | }
36 | });
37 | });
38 | }
39 | }
40 |
41 | export {
42 | BELoader,
43 | };
44 |
45 |
--------------------------------------------------------------------------------
/tests/TestCase/View/Helper/DatesHelperTest.php:
--------------------------------------------------------------------------------
1 | modify('-5 days')->format('Y-m-d');
30 | $result = $helper->daysAgo($date);
31 | $this->assertEquals(5, $result);
32 |
33 | // Test with a date 10 days ago
34 | $date = (new \DateTime())->modify('-10 days')->format('Y-m-d');
35 | $result = $helper->daysAgo($date);
36 | $this->assertEquals(10, $result);
37 |
38 | // Test with today's date
39 | $date = (new \DateTime())->format('Y-m-d');
40 | $result = $helper->daysAgo($date);
41 | $this->assertEquals(0, $result);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/resources/js/app/components/form/field-json.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
55 |
--------------------------------------------------------------------------------
/templates/Element/Form/resource_relations.twig:
--------------------------------------------------------------------------------
1 | {% for resourceName in resourceRelations %}
2 |
3 |
4 |
5 |
6 | {{ Layout.tr(resourceName)|lower }}
7 | <: totalObjects :>
9 |
10 |
11 |
12 |
13 | {# TODO: review, commented out, v-show is always false!!! #}
14 | {# #}
26 |
27 |
28 |
29 | {% endfor %}
30 |
31 |
--------------------------------------------------------------------------------
/webroot/svg/iconFloppy.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/Controller/Admin/CacheController.php:
--------------------------------------------------------------------------------
1 | for more details.
12 | */
13 | namespace App\Controller\Admin;
14 |
15 | use Cake\Cache\Cache;
16 | use Cake\Http\Response;
17 | use League\Flysystem\Filesystem;
18 | use League\Flysystem\Local\LocalFilesystemAdapter;
19 |
20 | /**
21 | * Cache Controller
22 | */
23 | class CacheController extends AdministrationBaseController
24 | {
25 | /**
26 | * Perform cache clear
27 | *
28 | * @return \Cake\Http\Response|null
29 | */
30 | public function clear(): ?Response
31 | {
32 | $prefixes = Cache::configured();
33 | foreach ($prefixes as $prefix) {
34 | Cache::clear($prefix);
35 | }
36 | $filesystem = new Filesystem(new LocalFilesystemAdapter(CACHE));
37 | $filesystem->deleteDirectory('twig_view');
38 |
39 | return $this->redirect($this->referer());
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/resources/js/app/helpers/text-helper.js:
--------------------------------------------------------------------------------
1 | export function humanizeString(string) {
2 | if (!string) {
3 | return '';
4 | }
5 | string = string
6 | .toLowerCase()
7 | .replace(/[_-]+/g, ' ')
8 | .replace(/\s{2,}/g, ' ')
9 | .trim();
10 | const words = string.split(' ');
11 | let res = '';
12 | words.forEach((entry) => {
13 | res += ' ' + upperCaseFirst(entry);
14 | });
15 |
16 | return res.trim();
17 | }
18 |
19 | export function upperCaseFirst(input) {
20 | return input.charAt(0).toUpperCase() + input.substr(1);
21 | }
22 |
23 |
24 | /**
25 | * Encode string to base64 using text encoder to ensure that the string is in utf-8 format
26 | * @param {string|undefined} value
27 | * @returns {string} base64 encoded string
28 | */
29 | export function utf8ToBase64(value = 'undefined') {
30 | const utf8Bytes = new TextEncoder().encode(value);
31 |
32 | return btoa(String.fromCharCode(...utf8Bytes));
33 | };
34 |
35 | /**
36 | * Decode string from base64 to utf-8
37 | * @param {string| undefined} value
38 | * @returns {string} decoded string
39 | */
40 | export function base64ToUtf8(value) {
41 | const binaryString = atob(value);
42 | const bytes = Uint8Array.from(binaryString, c => c.charCodeAt(0));
43 |
44 | return new TextDecoder().decode(bytes);
45 | };
46 |
--------------------------------------------------------------------------------
/tests/TestCase/Utility/RelationsToolsTest.php:
--------------------------------------------------------------------------------
1 | 1,
27 | 'type' => 'event',
28 | 'meta' => [
29 | 'relation' => [
30 | 'priority' => 1,
31 | ],
32 | ],
33 | ],
34 | [
35 | 'id' => 2,
36 | 'type' => 'document',
37 | 'meta' => [
38 | 'relation' => [
39 | 'priority' => 2,
40 | ],
41 | ],
42 | ],
43 | ];
44 | $expected = '1-event-{"priority":1},2-document-{"priority":2}';
45 | $actual = RelationsTools::toString($relations);
46 | static::assertEquals($expected, $actual);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/.github/workflows/php.yml:
--------------------------------------------------------------------------------
1 | name: 'php'
2 |
3 | on:
4 | pull_request:
5 | paths:
6 | - '**/*.php'
7 | - '.github/workflows/php.yml'
8 | - '.scrutinizer.yml'
9 | - 'composer.json'
10 | - 'phpcs.xml.dist'
11 | - 'phpstan.neon.dist'
12 | - 'psalm.xml'
13 | push:
14 | paths:
15 | - '**/*.php'
16 | - '.github/workflows/php.yml'
17 | - '.scrutinizer.yml'
18 | - 'composer.json'
19 | - 'phpcs.xml.dist'
20 | - 'phpstan.neon.dist'
21 | - 'psalm.xml'
22 |
23 | jobs:
24 | cs:
25 | uses: bedita/github-workflows/.github/workflows/php-cs.yml@v2
26 | with:
27 | php_versions: '["8.3"]'
28 |
29 | psalm:
30 | uses: bedita/github-workflows/.github/workflows/php-psalm.yml@v2
31 | with:
32 | php_versions: '["8.3"]'
33 |
34 | stan:
35 | uses: bedita/github-workflows/.github/workflows/php-stan.yml@v2
36 | with:
37 | php_versions: '["8.3"]'
38 |
39 | unit-5:
40 | uses: bedita/github-workflows/.github/workflows/php-unit.yml@v2
41 | with:
42 | php_versions: '["8.3"]'
43 | bedita_version: '5'
44 | coverage_min_percentage: 98
45 |
46 | unit-6:
47 | uses: bedita/github-workflows/.github/workflows/php-unit.yml@v2
48 | with:
49 | php_versions: '["8.3"]'
50 | bedita_version: '6'
51 | coverage_min_percentage: 98
52 |
--------------------------------------------------------------------------------
/templates/Element/Form/categories.twig:
--------------------------------------------------------------------------------
1 | {% if schema.categories is defined %}
2 | {% set objectCategories = object.attributes.categories|default([]) %}
3 | {% set schemaCategories = schema.categories|default([]) %}
4 |
5 |
6 |
7 |
10 | {{ __('Categories') }}
11 | {{ objectCategories|length }} / {{ schemaCategories|length }}
12 |
13 |
14 |
15 |
16 |
20 |
21 | {% do Form.unlockField('categories') %}
22 |
23 |
24 |
25 |
26 |
27 | {% endif %}
28 |
--------------------------------------------------------------------------------
/resources/js/app/components/clipboard-item/clipboard-item.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ label }}
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
52 |
--------------------------------------------------------------------------------
/src/View/AjaxView.php:
--------------------------------------------------------------------------------
1 | for more details.
12 | */
13 | namespace App\View;
14 |
15 | /**
16 | * A view class that is used for AJAX responses.
17 | * Currently only switches the default layout and sets the response type -
18 | * which just maps to text/html by default.
19 | */
20 | class AjaxView extends AppView
21 | {
22 | /**
23 | * The name of the layout file to render the view inside of. The name
24 | * specified is the filename of the layout in /templates/Layout without
25 | * the .ctp extension.
26 | *
27 | * @var string
28 | */
29 | public $layout = 'ajax';
30 |
31 | /**
32 | * Initialization hook method.
33 | *
34 | * @return void
35 | * @codeCoverageIgnore
36 | */
37 | public function initialize(): void
38 | {
39 | parent::initialize();
40 |
41 | $this->setResponse($this->getResponse()->withType('ajax'));
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Utility/Translate.php:
--------------------------------------------------------------------------------
1 | for more details.
12 | */
13 |
14 | namespace App\Utility;
15 |
16 | use Cake\Core\Configure;
17 | use Cake\Utility\Hash;
18 | use Cake\Utility\Inflector;
19 |
20 | /**
21 | * Translate class
22 | */
23 | class Translate
24 | {
25 | /**
26 | * Return translated string by input string.
27 | * Use `__` and `__d` to try to translate it.
28 | *
29 | * @param string $input The input string
30 | * @return string|null
31 | */
32 | public static function get(string $input): ?string
33 | {
34 | $text = Inflector::humanize($input);
35 | $translation = __($text);
36 | $plugins = (array)Configure::read('Plugins');
37 | $pluginName = Hash::get(array_keys($plugins), 0);
38 | if ($pluginName && $translation === $text) {
39 | return __d($pluginName, $text);
40 | }
41 |
42 | return $translation;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Controller/Admin/ApplicationsController.php:
--------------------------------------------------------------------------------
1 | for more details.
12 | */
13 | namespace App\Controller\Admin;
14 |
15 | /**
16 | * Applications Controller
17 | *
18 | * @property \App\Controller\Component\PropertiesComponent $Properties
19 | */
20 | class ApplicationsController extends AdministrationBaseController
21 | {
22 | /**
23 | * @inheritDoc
24 | */
25 | protected $resourceType = 'applications';
26 |
27 | /**
28 | * @inheritDoc
29 | */
30 | protected $readonly = false;
31 |
32 | /**
33 | * @inheritDoc
34 | */
35 | protected $properties = [
36 | 'name' => 'string',
37 | 'description' => 'text',
38 | 'enabled' => 'bool',
39 | ];
40 |
41 | /**
42 | * @inheritDoc
43 | */
44 | protected $propertiesSecrets = ['api_key', 'client_secret'];
45 |
46 | /**
47 | * @inheritDoc
48 | */
49 | protected $sortBy = 'name';
50 | }
51 |
--------------------------------------------------------------------------------
/resources/js/app/pages/trash/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Templates that uses this component (directly or indirectly)
3 | * Template/Trash/index.twig
4 | *
5 | * component used for TrashPage -> Index
6 | *
7 | * @extends ModulesIndex
8 | */
9 |
10 | import ModulesIndex from 'app/pages/modules/index';
11 | import { confirm } from 'app/components/dialog/dialog';
12 | import { t } from 'ttag';
13 |
14 | export default {
15 | extends: ModulesIndex,
16 | methods: {
17 | /**
18 | * Submit bulk restore form
19 | *
20 | * @return {void}
21 | */
22 | restoreItem() {
23 | if (this.selectedRows.length < 1) {
24 | return;
25 | }
26 | document.getElementById('form-restore').submit();
27 | },
28 |
29 | /**
30 | * Submit bulk delete form
31 | *
32 | * @return {void}
33 | */
34 | deleteItem() {
35 | let number = this.selectedRows.length;
36 | if (number < 1) {
37 | return;
38 | }
39 |
40 | let form = document.getElementById('form-delete');
41 | confirm(
42 | t`Do you confirm the deletion of ${number} item from the trash?`,
43 | t`yes, proceed`,
44 | form.submit.bind(form)
45 | );
46 | },
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/templates/Pages/Model/_model-view.scss:
--------------------------------------------------------------------------------
1 | body.view-model {
2 | .module-form form section.fieldset {
3 | input[disabled] {
4 | background-color: $gray-700;
5 | color: $gray-100;
6 | }
7 |
8 | .link-to-parent {
9 | display: block;
10 | margin-top: -.75 * $gutter;
11 | }
12 | }
13 |
14 | .modules-view section:not(:first-child) {
15 | margin-top: $gutter * 3;
16 | }
17 |
18 |
19 | .relations {
20 | &-container {
21 | .relations-group {
22 | display: grid;
23 | grid-gap: 2px;
24 | grid-template-columns: repeat(auto-fill, minmax(200px, 1fr) );
25 | }
26 | }
27 | }
28 |
29 | .properties {
30 | &-container {
31 | display: grid;
32 | grid-gap: $gutter;
33 | grid-template-columns: repeat(1, 1fr);
34 | @media screen and (min-width: 768px) {
35 | grid-template-columns: repeat(2, 1fr);
36 | }
37 | @media screen and (min-width: 1024px) {
38 | grid-template-columns: repeat(4, 1fr);
39 | }
40 | }
41 |
42 | .buttons-container {
43 | display: flex;
44 | flex-direction: column;
45 | button { margin-bottom: $gutter * .5; }
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/templates/Element/Form/calendar.twig:
--------------------------------------------------------------------------------
1 | {# Calendar: use date_ranges if `DateRanges` association is set #}
2 |
3 | {% if in_array('DateRanges', schema.associations) %}
4 | {% set key = 'Properties.%s.options.date_ranges.label'|format(object.type) %}
5 | {% set plugin = config('Properties.%s.options.date_ranges.plugin'|format(object.type)) %}
6 | {% set label = plugin ? __d(plugin, config(key)) : __(config(key)|default('Calendar')) %}
7 | {% set key2 = 'Properties.%s.options.date_ranges.options'|format(object.type) %}
8 | {% set options = config(key2)|default({"weekdays":{}}) %}
9 |
10 |
11 |
16 |
17 |
18 |
23 |
24 | {% do Form.unlockField('date_ranges') %}
25 |
26 |
27 |
28 | {% endif %}
29 |
--------------------------------------------------------------------------------
/templates/Element/Modules/index_bulk.twig:
--------------------------------------------------------------------------------
1 | {% if schema|json_encode == '[false]' %}
2 | {% set bulkOptions = {
3 | 'schema': {
4 | 'properties': {
5 | 'status': {
6 | 'type': 'string',
7 | 'enum': [
8 | 'on',
9 | 'off',
10 | 'draft',
11 | ],
12 | '$id': '/properties/status',
13 | 'title': 'Status',
14 | 'description': 'object status: on, draft, off',
15 | 'default': 'draft',
16 | },
17 | },
18 | }
19 | } %}
20 | {% else %}
21 | {% set bulkOptions = { 'schema': schema } %}
22 | {% endif %}
23 |
24 |
25 |
26 | {{ element('Form/bulk_export') }}
27 |
28 |
29 |
30 |
31 |
32 | {{ element('Form/bulk_properties', bulkOptions) }}
33 |
34 | {{ element('Form/bulk_custom', bulkOptions) }}
35 |
36 | {{ element('Form/bulk_category', bulkOptions) }}
37 |
38 | {{ element('Form/bulk_position', bulkOptions) }}
39 |
40 | {{ element('Form/bulk_trash', bulkOptions) }}
41 |
42 |
43 |
--------------------------------------------------------------------------------
/resources/js/app/components/form/field-select.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
15 | {{ item }}
16 |
17 |
18 |
19 |
61 |
--------------------------------------------------------------------------------
/src/Controller/Admin/AsyncJobsController.php:
--------------------------------------------------------------------------------
1 | for more details.
12 | */
13 | namespace App\Controller\Admin;
14 |
15 | /**
16 | * Async Jobs Controller
17 | *
18 | * @property \App\Controller\Component\PropertiesComponent $Properties
19 | */
20 | class AsyncJobsController extends AdministrationBaseController
21 | {
22 | /**
23 | * Resource type in use
24 | *
25 | * @var string
26 | */
27 | protected $resourceType = 'async_jobs';
28 |
29 | /**
30 | * @inheritDoc
31 | */
32 | protected $deleteonly = true;
33 |
34 | /**
35 | * @inheritDoc
36 | */
37 | protected $properties = [
38 | 'service' => 'string',
39 | 'scheduled_from' => 'date',
40 | 'expires' => 'date',
41 | 'max_attempts' => 'integer',
42 | 'locked_until' => 'date',
43 | ];
44 |
45 | /**
46 | * @inheritDoc
47 | */
48 | protected $meta = ['created', 'modified', 'completed'];
49 | }
50 |
--------------------------------------------------------------------------------
/src/Controller/Component/ParentsComponent.php:
--------------------------------------------------------------------------------
1 | for more details.
13 | */
14 |
15 | namespace App\Controller\Component;
16 |
17 | use App\Utility\ApiClientTrait;
18 | use Cake\Controller\Component;
19 |
20 | /**
21 | * Parents component.
22 | * This component is used to add/remove parents to a folder.
23 | */
24 | class ParentsComponent extends Component
25 | {
26 | use ApiClientTrait;
27 |
28 | /**
29 | * Add parent to a folder.
30 | *
31 | * @param string $folderId Folder ID.
32 | * @param array $parents Parent objects as id/type pairs.
33 | * @return array
34 | */
35 | public function addRelated(string $folderId, array $parents): array
36 | {
37 | $results = [];
38 | foreach ($parents as $parent) {
39 | $results[] = $this->getClient()->replaceRelated($folderId, 'folders', 'parent', $parent);
40 | }
41 |
42 | return $results;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/templates/Element/Form/form_file_upload.twig:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ Form.file('file', {
5 | ':accept': 'fileAcceptMimeTypes("' ~ object.type ~ '")',
6 | 'class': 'file-input',
7 | 'v-on:change': 'onFileChange($event, "' ~ object.type ~ '")'
8 | }) | raw }}
9 |
10 |
11 | {{ __('Choose a file') }}
12 |
13 |
14 | <: file?.name :>
15 |
16 | {% if object.type == 'images' %}
17 |
18 |
19 |
20 | {% elseif not object.id %}
21 |
22 | <: file?.name :>
23 |
24 | {% endif %}
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/tests/Utils/DummyImportFilter.php:
--------------------------------------------------------------------------------
1 | createAsyncJob($filename, $filepath, $options);
29 | }
30 |
31 | /**
32 | * Call parent, to bypass method protection, for test purpose
33 | *
34 | * @param string $filename The file name
35 | * @param string $filepath The file path
36 | * @param array $options The options
37 | * @return \App\Core\Result\ImportResult
38 | * @throws \LogicException When method is called but missing the async job service name.
39 | */
40 | public function createAsyncJob($filename, $filepath, ?array $options = []): ImportResult
41 | {
42 | return parent::createAsyncJob($filename, $filepath, $options);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/resources/js/app/components/object-types-list/object-types-list.js:
--------------------------------------------------------------------------------
1 | export default {
2 | template: ``,
12 |
13 | props: {
14 | all: [],
15 | selected: [],
16 | side: '',
17 | },
18 |
19 | data() {
20 | return {
21 | current: [],
22 | };
23 | },
24 |
25 | mounted() {
26 | this.$nextTick(() => {
27 | this.current = this.selected;
28 | });
29 | },
30 |
31 | methods: {
32 | className(t) {
33 | return `tag has-background-module-${t}`;
34 | },
35 | name(prefix) {
36 | return `${prefix}_${this.side}`;
37 | },
38 | ref(t) {
39 | return `/model/object_types/view/${t}`;
40 | },
41 | valueCurrent() {
42 | return this.current.join(',');
43 | },
44 | valueChange() {
45 | return this.selected.join(',');
46 | },
47 | },
48 | }
49 |
--------------------------------------------------------------------------------
/src/Core/Result/ImportResult.php:
--------------------------------------------------------------------------------
1 | filename = $filename;
42 | }
43 |
44 | /**
45 | * Reset attributes
46 | *
47 | * @return void
48 | */
49 | public function reset(): void
50 | {
51 | $this->filename = $this->info = $this->warn = $this->error = '';
52 | $this->created = $this->updated = $this->errors = 0;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/resources/js/app/components/permission/permission.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
54 |
--------------------------------------------------------------------------------
/src/Controller/Model/ExportController.php:
--------------------------------------------------------------------------------
1 | for more details.
12 | */
13 | namespace App\Controller\Model;
14 |
15 | use App\Controller\AppController;
16 | use Cake\Http\Response;
17 |
18 | /**
19 | * Export cotroller: download project model in JSON format
20 | */
21 | class ExportController extends AppController
22 | {
23 | /**
24 | * Downloaded file name
25 | *
26 | * @var string
27 | */
28 | public const FILENAME = 'project_model.json';
29 |
30 | /**
31 | * Export project model JSON file
32 | *
33 | * @return \Cake\Http\Response
34 | */
35 | public function model(): Response
36 | {
37 | $content = $this->apiClient->get('/model/project', [], ['Accept' => 'application/json']);
38 |
39 | $response = $this->getResponse()->withStringBody(json_encode($content));
40 | $response = $response->withType('application/json');
41 |
42 | return $response->withDownload(self::FILENAME);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Utility/RelationsTools.php:
--------------------------------------------------------------------------------
1 | for more details.
15 | */
16 |
17 | namespace App\Utility;
18 |
19 | use Cake\Utility\Hash;
20 |
21 | /**
22 | * Relations tools
23 | */
24 | class RelationsTools
25 | {
26 | /**
27 | * Convert relations to string.
28 | *
29 | * @param array $relations Relations to convert.
30 | * @return string
31 | */
32 | public static function toString(array $relations): string
33 | {
34 | $rrs = [];
35 | foreach ($relations as $item) {
36 | $id = (string)Hash::get($item, 'id');
37 | $type = (string)Hash::get($item, 'type');
38 | $meta = json_encode((array)Hash::get($item, 'meta.relation', []));
39 | $rrs[] = sprintf(
40 | '%s-%s-%s',
41 | $id,
42 | $type,
43 | $meta
44 | );
45 | }
46 |
47 | return implode(',', $rrs);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Controller/MultiuploadController.php:
--------------------------------------------------------------------------------
1 | for more details.
13 | */
14 |
15 | namespace App\Controller;
16 |
17 | /**
18 | * Multiupload Controller
19 | */
20 | class MultiuploadController extends AppController
21 | {
22 | /**
23 | * Object type currently used
24 | *
25 | * @var string
26 | */
27 | protected $objectType = null;
28 |
29 | /**
30 | * @inheritDoc
31 | */
32 | public function initialize(): void
33 | {
34 | parent::initialize();
35 | $this->objectType = $this->getRequest()->getParam('object_type');
36 | $this->Modules->setConfig('currentModuleName', ucfirst($this->objectType));
37 | $this->set('currentModule', ['name' => $this->objectType]);
38 | $this->set('objectType', $this->objectType);
39 | }
40 |
41 | /**
42 | * {@inheritDoc}
43 | *
44 | * @codeCoverageIgnore
45 | */
46 | public function index(): void
47 | {
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Plugin.php:
--------------------------------------------------------------------------------
1 | for more details.
12 | */
13 |
14 | namespace App;
15 |
16 | use Cake\Core\Plugin as CakePlugin;
17 |
18 | /**
19 | * {@inheritDoc}
20 | *
21 | * Extended with BEdita plugins utilities
22 | *
23 | * @codeCoverageIgnore
24 | */
25 | class Plugin extends CakePlugin
26 | {
27 | /**
28 | * Loaded BEdita Manager application plugins
29 | * Auxiliary & internally loaded plugins like `DebugKit`, `Bake` and `TwigView` are exluded
30 | *
31 | * @return array
32 | */
33 | public static function loadedAppPlugins(): array
34 | {
35 | $sysPlugins = [
36 | 'Authentication',
37 | 'Bake',
38 | 'DebugKit',
39 | 'IdeHelper',
40 | 'Cake/Repl',
41 | 'BEdita/WebTools',
42 | 'BEdita/I18n',
43 | 'Migrations',
44 | 'Cake/TwigView',
45 | ];
46 |
47 | return array_values(array_diff(static::loaded(), $sysPlugins));
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/templates/Element/custom_colors.twig:
--------------------------------------------------------------------------------
1 |
28 |
--------------------------------------------------------------------------------
/templates/Element/Form/history.scss:
--------------------------------------------------------------------------------
1 | .history {
2 | .history-content {
3 | $module-color: var(--module-color);
4 |
5 | summary {
6 | cursor: pointer;
7 |
8 | /* Things to remove summary marker */
9 | &::-webkit-details-marker {
10 | display: none;
11 | }
12 |
13 | &::marker {
14 | display: none;
15 | }
16 |
17 | /* Firefox */
18 | list-style-type: none;
19 | }
20 |
21 | ul.history-items {
22 | list-style: none;
23 | margin: 0;
24 | padding: 0 0 0 1rem;
25 |
26 | li.history-item {
27 | display: inline-grid;
28 | grid-gap: 1rem;
29 | grid-template-columns: auto auto 1fr auto;
30 | border-left: 1px solid;
31 |
32 | &::before {
33 | content: '\25CF';
34 | color: $module-color;
35 | margin: 2px 0 0 -6px;
36 | font-size: 12px;
37 | }
38 |
39 | &:first-of-type {
40 | padding-top: 1rem;
41 | }
42 |
43 | &:last-of-type {
44 | padding-bottom: 1rem;
45 | border-image: linear-gradient(to bottom, $gray-600 0 1.4rem, transparent 1.4rem 100%) 1;
46 | }
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/resources/js/app/mixins/fetch.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Fetch mixin used to comunicate with cake controller's method
3 | *
4 | * TO-DO: handle token timeout
5 | *
6 | * @requires BEDITA global object
7 | */
8 |
9 | import axios from 'axios';
10 |
11 | // private var to prevent tempering
12 | let privateAxios;
13 |
14 | export const FetchMixin = {
15 | created() {
16 | privateAxios = this.createCustomAxios();
17 | },
18 |
19 | computed: {
20 | axios() {
21 | return privateAxios;
22 | }
23 | },
24 |
25 | methods: {
26 | /**
27 | * set up axios api
28 | *
29 | * @return {Object} custom axios instance
30 | */
31 | createCustomAxios() {
32 | return axios.create({
33 | baseURL: BEDITA.base,
34 | timeout: BEDITA?.uploadConfig?.timeout || 30000,
35 | credentials: 'same-origin',
36 | headers: {
37 | 'accept': 'application/json',
38 | 'content-type': 'multipart/form-data',
39 | 'X-CSRF-Token': this.getCSFRToken(),
40 | },
41 |
42 | validateStatus: (status) => {
43 | return status >= 200 && status < 300; // default
44 | },
45 | });
46 | },
47 |
48 | /**
49 | * get axios Factory
50 | */
51 | getAxios() {
52 | return axios;
53 | },
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/resources/js/app/components/form-file-upload.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Templates that uses this component (directly or indirectly):
3 | * ...
4 | *
5 | *
6 | *
7 | * form form-file-upload element
8 | *
9 | * @prop {Array} labels
10 | * @prop {int} defaultActive index of the default active label in labels
11 | */
12 | export default {
13 |
14 | data() {
15 | return {
16 | file: null,
17 | }
18 | },
19 |
20 | methods: {
21 | fileAcceptMimeTypes(type) {
22 | return this.$helpers.acceptMimeTypes(type);
23 | },
24 |
25 | onFileChange(e, type) {
26 | this.file = null;
27 | if (this.$helpers.checkMimeForUpload(e.target.files[0], type) === false) {
28 | return;
29 | }
30 | if (type === 'images') {
31 | this.$helpers.checkImageResolution(e.target.files[0]);
32 | }
33 | if (this.$helpers.checkMaxFileSize(e.target.files[0]) === false) {
34 | return;
35 | }
36 | this.file = e.target.files[0];
37 | this.$helpers.setTitleFromFileName('title', this.file.name);
38 | },
39 |
40 | resetFile(e) {
41 | e.target.parentNode.querySelector('input.file-input').value = '';
42 | this.file = null;
43 | },
44 |
45 | previewImage() {
46 | return this.$helpers.updatePreviewImage(this.file, 'title');
47 | },
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/resources/js/app/components/secret/secret.js:
--------------------------------------------------------------------------------
1 | import { t } from 'ttag';
2 |
3 | export default {
4 | template: `
5 |
6 |
<: val :>
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
****************
17 |
18 |
19 |
20 |
21 |
---
22 |
23 |
24 |
25 |
`,
26 |
27 | props: {
28 | val: '',
29 | },
30 |
31 | data() {
32 | return {
33 | visible: false,
34 | msg: '',
35 | };
36 | },
37 |
38 | methods: {
39 | copy() {
40 | navigator.clipboard.writeText(this.val);
41 | this.msg = t`copied in the clipboard`;
42 | },
43 | reset(v) {
44 | this.visible = v;
45 | this.msg = '';
46 | }
47 | },
48 | }
49 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # User specific & automatically generated files #
2 | #################################################
3 | /.vagrant/*
4 | /composer.lock
5 | /composer.local.json
6 | /config/app_local.php
7 | /config/.env
8 | /config/projects/*
9 | /coverage/*
10 | /index.html
11 | /logs/*
12 | !/logs/.gitkeep
13 | /phpstan.neon
14 | /phpunit.xml
15 | /provision/*
16 | /tests/.env
17 | /tmp/*
18 | !/tmp/.gitkeep
19 | /vendor/*
20 | !/vendor/.gitkeep
21 | /.phpunit.result.cache
22 |
23 | # plugins folders #
24 | ##########################
25 | /plugins/*
26 | !/logs/.gitkeep
27 |
28 | # IDE and editor specific files #
29 | #################################
30 | /nbproject
31 | /tags
32 | .idea
33 | .project
34 | .buildpath
35 | .settings
36 | .vscode
37 | .history
38 |
39 | # OS generated files #
40 | ######################
41 | .DS_Store
42 | .DS_Store?
43 | .directory
44 | ._*
45 | .Spotlight-V100
46 | .Trashes
47 | Icon?
48 | ehthumbs.db
49 | Thumbs.db
50 |
51 | # Elastic Beanstalk Files
52 | .elasticbeanstalk/*
53 | !.elasticbeanstalk/*.cfg.yml
54 | !.elasticbeanstalk/*.global.yml
55 |
56 | # node, preprocessors... #
57 | #################################
58 | package-lock.json
59 | node_modules/
60 | .cache/
61 | yarn-error.log
62 | *.css.map
63 | *.bundle.js.map
64 | bundle-report.*.html
65 |
66 | # bundle files
67 | /webroot/bundle-report
68 | /webroot/*.hot-update.*
69 | /webroot/css/*
70 | !/webroot/css/be-icons-codes.css
71 | !/webroot/css/be-icons-font.css
72 | /webroot/js/*.js
73 | /webroot/js/vendors
74 | /webroot/manifest.json
75 |
--------------------------------------------------------------------------------
/bin/bash_completion.sh:
--------------------------------------------------------------------------------
1 | #
2 | # Bash completion file for CakePHP console.
3 | # Copy this file to a file named `cake` under `/etc/bash_completion.d/`.
4 | # For more info check https://book.cakephp.org/4/en/console-commands/completion.html#how-to-enable-bash-autocompletion-for-the-cakephp-console
5 | #
6 |
7 | _cake()
8 | {
9 | local cur prev opts cake
10 | COMPREPLY=()
11 | cake="${COMP_WORDS[0]}"
12 | cur="${COMP_WORDS[COMP_CWORD]}"
13 | prev="${COMP_WORDS[COMP_CWORD-1]}"
14 |
15 | if [[ "$cur" == -* ]] ; then
16 | if [[ ${COMP_CWORD} = 1 ]] ; then
17 | opts=$(${cake} completion options)
18 | elif [[ ${COMP_CWORD} = 2 ]] ; then
19 | opts=$(${cake} completion options "${COMP_WORDS[1]}")
20 | else
21 | opts=$(${cake} completion options "${COMP_WORDS[1]}" "${COMP_WORDS[2]}")
22 | fi
23 |
24 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
25 | return 0
26 | fi
27 |
28 | if [[ ${COMP_CWORD} = 1 ]] ; then
29 | opts=$(${cake} completion commands)
30 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
31 | return 0
32 | fi
33 |
34 | if [[ ${COMP_CWORD} = 2 ]] ; then
35 | opts=$(${cake} completion subcommands $prev)
36 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
37 | if [[ $COMPREPLY = "" ]] ; then
38 | _filedir
39 | return 0
40 | fi
41 | return 0
42 | fi
43 |
44 | return 0
45 | }
46 |
47 | complete -F _cake cake bin/cake
48 |
--------------------------------------------------------------------------------
/config/requirements.php:
--------------------------------------------------------------------------------
1 | .bulk #}
2 | {% for label, key in bulkActions %}
3 |
4 | {% if schema.properties[key] is not defined %}
5 |
6 | {% set bulkFormId = 'bulk-' ~ key|dasherize|replace({'.': '-', '/': '-', '\\': '-'}) %}
7 | {{ Form.create(null, {
8 | 'id': bulkFormId,
9 | 'url': {'_name': 'modules:bulkCustom', 'object_type': objectType, '?': _view.request.getQuery()},
10 | })|raw }}
11 |
12 |
13 |
14 | {% do Form.unlockField('ids') %}
15 | {% do Form.unlockField('custom_action') %}
16 |
17 |
18 | {%- if label matches '/^\\d+$/' -%}
19 | {{ __(key|humanize) }}
20 | {%- else -%}
21 | {{ __(label|humanize) }}
22 | {%- endif -%}
23 |
24 |
25 |
26 | {{ __('Ok') }}
27 |
28 |
29 | {{ Form.end()|raw }}
30 | {% endif %}
31 | {% endfor %}
32 |
--------------------------------------------------------------------------------
/templates/Element/Form/captions.twig:
--------------------------------------------------------------------------------
1 | {% if object.id %}
2 | {% if schema.properties.captions %}
3 | {% set captions = object.attributes.captions %}
4 |
5 |
6 |
7 | {{ __('Captions') }}
8 | {{ captions|length }}
9 |
10 |
11 |
12 |
20 |
21 | {% do Form.unlockField('captions') %}
22 | {{ write_config('_jsonKeys', config('_jsonKeys', [])|merge(['captions'])) }}
23 |
24 |
25 |
26 |
27 | {% endif %}
28 | {% endif %}
29 |
--------------------------------------------------------------------------------
/templates/Element/Modules/index_header.twig:
--------------------------------------------------------------------------------
1 |
27 |
--------------------------------------------------------------------------------
/src/Controller/Admin/AuthProvidersController.php:
--------------------------------------------------------------------------------
1 | for more details.
12 | */
13 | namespace App\Controller\Admin;
14 |
15 | /**
16 | * AuthProviders Controller
17 | *
18 | * @property \App\Controller\Component\PropertiesComponent $Properties
19 | */
20 | class AuthProvidersController extends AdministrationBaseController
21 | {
22 | /**
23 | * @inheritDoc
24 | */
25 | protected $resourceType = 'auth_providers';
26 |
27 | /**
28 | * @inheritDoc
29 | */
30 | protected $readonly = false;
31 |
32 | /**
33 | * @inheritDoc
34 | */
35 | protected $properties = [
36 | 'name' => 'string',
37 | 'auth_class' => 'string',
38 | 'url' => 'string',
39 | 'params' => 'json',
40 | 'enabled' => 'bool',
41 | ];
42 |
43 | /**
44 | * @inheritDoc
45 | */
46 | protected $propertiesForceJson = [
47 | 'params',
48 | ];
49 |
50 | /**
51 | * @inheritDoc
52 | */
53 | protected $meta = [];
54 |
55 | /**
56 | * @inheritDoc
57 | */
58 | protected $sortBy = 'name';
59 | }
60 |
--------------------------------------------------------------------------------
/webroot/index.php:
--------------------------------------------------------------------------------
1 | emit($server->run());
41 |
--------------------------------------------------------------------------------
/resources/js/app/components/json-editor/json-editor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
59 |
--------------------------------------------------------------------------------
/src/Form/Form.php:
--------------------------------------------------------------------------------
1 | for more details.
12 | */
13 |
14 | namespace App\Form;
15 |
16 | use Cake\Utility\Inflector;
17 |
18 | /**
19 | * Form class provides utilities for \App\Form classes.
20 | */
21 | class Form
22 | {
23 | /**
24 | * Form null value
25 | */
26 | public const NULL_VALUE = ':::null:::';
27 |
28 | /**
29 | * Return method [$className, $methodName], if it's callable.
30 | * Otherwise, throw \InvalidArgumentException
31 | *
32 | * @param string $className The class name
33 | * @param string $name The method name
34 | * @param string|null $format The format
35 | * @return array
36 | */
37 | public static function getMethod(string $className, string $name, $format = ''): array
38 | {
39 | $methodName = !empty($format) ? $format : Inflector::variable(str_replace('-', '_', $name));
40 | $method = [$className, $methodName];
41 | if (is_callable($method)) {
42 | return $method;
43 | }
44 |
45 | throw new \InvalidArgumentException(sprintf('Method "%s" is not callable', $methodName));
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/tests/TestCase/Core/I18n/DummyTranslator.php:
--------------------------------------------------------------------------------
1 | options = $options;
26 | }
27 |
28 | /**
29 | * Translate an array of texts $texts from language source $from to language target $to
30 | *
31 | * @param array $texts The texts to translate
32 | * @param string $from The source language
33 | * @param string $to The target language
34 | * @return string The translation in json format as string, i.e.
35 | * {
36 | * "translation": [
37 | * "",
38 | * "",
39 | * [...]
40 | * ""
41 | * ]
42 | * }
43 | */
44 | public function translate(array $texts, string $from, string $to): string
45 | {
46 | $translation = [];
47 | foreach ($texts as $text) {
48 | $translation[] = sprintf('text: %s, from: %s, to: %s', $text, $from, $to);
49 | }
50 |
51 | return json_encode(compact('translation'));
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/resources/styles/_base.scss:
--------------------------------------------------------------------------------
1 | // base
2 |
3 | body {
4 | overflow-x: hidden;
5 | font-family: $font-family-base;
6 | font-size: $font-size-sm;
7 | line-height: $line-height-base;
8 | background-color: $defaultBackground;
9 | // background-image: linear-gradient(160deg, $gray-900 0%, $gray-800 100%);
10 | background-attachment: fixed;
11 | background-repeat: no-repeat;
12 | color: $white;
13 | font-weight: $font-weight-base;
14 | text-align: left; // explicit initial text-align value so that we can later use `inherit`
15 | }
16 |
17 | *,
18 | *::before,
19 | *::after { box-sizing: border-box; }
20 |
21 | h1 { margin-bottom: $gutter; }
22 | h1, h2, h3 {
23 | margin-top: 0;
24 | line-height: 1.125em;
25 | font-weight: $font-weight-base;
26 | }
27 |
28 | p { margin: 0; }
29 |
30 | a { color: inherit; }
31 | a,
32 | a:link, a:hover, a:active, a:visited {
33 | text-decoration: none;
34 | }
35 |
36 | b, strong {
37 | font-weight: $font-weight-bold;
38 | }
39 |
40 | figure { margin: 0; }
41 |
42 | fieldset { border: none; }
43 |
44 | textarea:focus,
45 | input:focus { outline: none; box-shadow: inset 0 0 6px rgba(0, 0, 0, .2); }
46 |
47 | label, th { font-weight: normal; }
48 |
49 | div, section {
50 | &:focus { outline: none; }
51 | }
52 |
53 | dl {
54 | margin: 0;
55 | dd {
56 | margin: 0;
57 | font-weight: $font-weight-bold;
58 | }
59 | dd + dt {
60 | margin-top: $gutter * .25;
61 | }
62 | }
63 |
64 | [class^='icon-'],
65 | [class*=' icon-'] {
66 | line-height: 1;
67 | }
68 |
--------------------------------------------------------------------------------
/templates/Pages/Import/import.scss:
--------------------------------------------------------------------------------
1 | #data-import {
2 | .fieldset {
3 | margin-bottom: $gutter * 1.5;
4 |
5 | .tab {
6 | cursor: auto;
7 | }
8 | }
9 |
10 | .tab-container {
11 | label {
12 | display: block;
13 | cursor: pointer;
14 | & ~ label {
15 | margin-top: $gutter * .5;
16 | }
17 | }
18 | }
19 |
20 | button.submit {
21 | padding-left: 3 * $gutter;
22 | padding-right: 3 * $gutter;
23 | }
24 | }
25 |
26 | #list-jobs {
27 | min-width: 100%;
28 | display: grid;
29 | grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 50px;
30 |
31 | .table-header {
32 | border-bottom: 1px solid $gray-700;
33 | }
34 |
35 | > div {
36 | padding: $gutter * .5;
37 |
38 | &.job-payload {
39 | grid-column: span 7;
40 | color: #000;
41 | font-size: xx-small;
42 | font-family: monospace;
43 |
44 | > h3 {
45 | border: solid #999 1px;
46 | color: #999;
47 | margin: 0px 0px;
48 | padding: 10px 10px;
49 | text-align: center;
50 | }
51 | }
52 |
53 | &.completed {
54 | color: $success;
55 | }
56 |
57 | &.failed {
58 | color: $danger;
59 | }
60 |
61 | &.locked, &.pending {
62 | color: $pending;
63 | }
64 |
65 | &.planned {
66 | color: $info;
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/resources/js/app/components/menu.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Templates that uses this component (directly or indirectly):
3 | * Template/Element/menu/menu.twig
4 | *
5 | * component
6 | *
7 | */
8 | export default {
9 |
10 | data() {
11 | return {
12 | popUpAction: '',
13 | searchString: '',
14 | };
15 | },
16 | methods: {
17 | togglePopup(action) {
18 | if (action == this.popUpAction) {
19 | this.popUpAction = '';
20 | } else {
21 | this.popUpAction = action;
22 | this.$nextTick(() => {
23 | this.$refs.searchInput.focus();
24 | });
25 | }
26 | },
27 |
28 | captureKeys(e) {
29 | let key = e.which || e.keyCode || 0;
30 | switch (key) {
31 | case 13:
32 | if (!e.shiftKey) {
33 | this.go();
34 | }
35 | break;
36 | case 27:
37 | this.popUpAction = '';
38 | break;
39 | }
40 | },
41 |
42 | go() {
43 | let urlPath = '';
44 | if (this.popUpAction == 'search') {
45 | urlPath += '/objects?q=';
46 | } else if (this.popUpAction == 'id') {
47 | urlPath += '/view/';
48 | }
49 |
50 | if (this.searchString && urlPath) {
51 | window.location.href = BEDITA.base + urlPath + this.searchString;
52 | }
53 | },
54 | },
55 | }
56 |
--------------------------------------------------------------------------------
/templates/Element/Menu/colophon.twig:
--------------------------------------------------------------------------------
1 | {% set apiCheck = System.checkBeditaApiVersion() %}
2 | {% set apiClass = apiCheck == 1 ? 'ok' : 'error' %}
3 | {% set authors = [Html.link('Chialab', 'https://www.chialab.it', {'target': '_blank'}), Html.link('ChannelWeb', 'https://www.channelweb.it', {'target': '_blank'})] %}
4 | {% set warn = __('API version required: {0}', config('BEditaAPI.versions')|join(' | ')) %}
5 |
6 |
28 |
--------------------------------------------------------------------------------
/tests/TestCase/Controller/Admin/CacheControllerTest.php:
--------------------------------------------------------------------------------
1 | Cache);
38 | Cache::disable();
39 |
40 | parent::tearDown();
41 | }
42 |
43 | /**
44 | * Test clear
45 | *
46 | * @return void
47 | * @covers ::clear()
48 | */
49 | public function testClear(): void
50 | {
51 | // write something in cache
52 | Cache::write('something', 'true');
53 | $this->Cache = new CacheController(
54 | new ServerRequest(
55 | [
56 | 'environment' => [
57 | 'REQUEST_METHOD' => 'GET',
58 | ],
59 | ]
60 | )
61 | );
62 | $this->Cache->clear();
63 | static::assertEmpty(Cache::read('something'));
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/resources/js/app/components/staggered-list.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Templates that uses this component (directly or indirectly):
3 | * Template/Elements/relations.twig
4 | *
5 | * component used for lists with staggered animation
6 | *
7 | */
8 |
9 | const NAME = 'staggered';
10 |
11 | export default {
12 | template: `
13 |
17 |
18 | `,
19 |
20 | props: {
21 | stagger: {
22 | type: String,
23 | default: () => 50,
24 | },
25 | },
26 |
27 | methods: {
28 | enter(el, done) {
29 | el.classList.remove(`${NAME}-enter-to`);
30 | el.classList.add(`${NAME}-enter`);
31 | const delay = this.getDelay(el);
32 | setTimeout(() => {
33 | this.$nextTick(() => {
34 | el.classList.add(`${NAME}-enter`);
35 | el.classList.remove(`${NAME}-enter-to`);
36 | el.classList.remove(`${NAME}-enter-active`);
37 | });
38 |
39 | done();
40 | }, delay);
41 | },
42 |
43 | afterEnter(el) {
44 | this.$nextTick(() => {
45 | el.classList.remove(`${NAME}-enter`);
46 | el.classList.remove(`${NAME}-enter-to`);
47 | });
48 | },
49 |
50 | getDelay(el) {
51 | return el.dataset && el.dataset.index * this.stagger + 5;
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/templates/Element/Form/bulk_position.twig:
--------------------------------------------------------------------------------
1 | {% if modules.folders %}
2 | {{ Form.create(null, {
3 | 'id': 'bulk-folder',
4 | 'url': {'_name': 'modules:bulkPosition', 'object_type': objectType, '?': _view.request.getQuery()},
5 | })|raw }}
6 |
7 |
8 | {{ __('Copy to') }}
9 |
10 |
11 |
12 |
13 | {{ __('Move to') }}
14 |
15 |
16 | {% do Form.unlockField('action') %}
17 |
18 |
23 | {% do Form.unlockField('folderSelected') %}
24 |
25 |
26 | {% do Form.unlockField('ids') %}
27 |
28 |
33 |
34 | {{ __('Ok') }}
35 |
36 |
37 | {{ Form.end()|raw }}
38 | {% endif %}
39 |
--------------------------------------------------------------------------------
/resources/js/app/pages/dashboard/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | name: 'DashboardIndex',
3 |
4 | components: {
5 | RecentActivity:() => import(/* webpackChunkName: "recent-activity" */'app/components/recent-activity/recent-activity'),
6 | },
7 |
8 | props: {
9 | q: {
10 | type: String,
11 | default: '',
12 | },
13 | },
14 |
15 | data() {
16 | return {
17 | searchId: '',
18 | searchString: '',
19 | };
20 | },
21 |
22 | created() {
23 | if (document.referrer.endsWith('/login') && window.top !== window) {
24 | window.top.postMessage('login', BEDITA.base);
25 | }
26 |
27 | this.searchString = this.q;
28 | },
29 |
30 | methods: {
31 | captureKeys(e) {
32 | let key = e.which || e.keyCode || 0;
33 | switch (key) {
34 | case 13:
35 | this.searchObjects();
36 | break;
37 | case 27:
38 | this.popUpAction = '';
39 | break;
40 | }
41 | },
42 | goToID() {
43 | if (!this.searchId) {
44 | return;
45 | }
46 | window.location.href = `${BEDITA.base}/view/${this.searchId}`;
47 | },
48 | searchObjects() {
49 | if (this.searchString) {
50 | this.$refs.searchSubmit.classList.add('is-loading-spinner');
51 | window.location.href = BEDITA.base + '/objects?q=' + this.searchString;
52 | }
53 | },
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/tests/Utils/ModulesControllerSample.php:
--------------------------------------------------------------------------------
1 | Schema->descendants($this->objectType);
22 | }
23 |
24 | /**
25 | * Public version of parent function (protected)
26 | *
27 | * @return string
28 | */
29 | public function availableRelationshipsUrl(string $relation): string
30 | {
31 | return parent::availableRelationshipsUrl($relation);
32 | }
33 |
34 | /**
35 | * Get api client.
36 | *
37 | * @return \BEdita\SDK\BEditaClient
38 | */
39 | public function getApiClient(): BEditaClient
40 | {
41 | return $this->apiClient;
42 | }
43 |
44 | /**
45 | * Set api client.
46 | *
47 | * @param \BEdita\SDK\BEditaClient|null $client Api client.
48 | * @return void
49 | */
50 | public function setApiClient(?BEditaClient $client = null): void
51 | {
52 | $this->apiClient = $client;
53 | }
54 |
55 | /**
56 | * Create new object from ajax request.
57 | *
58 | * @return void
59 | */
60 | public function save(): void
61 | {
62 | parent::save();
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/tests/TestCase/Controller/ErrorControllerTest.php:
--------------------------------------------------------------------------------
1 | for more details.
12 | */
13 |
14 | namespace App\Test\TestCase\Controller;
15 |
16 | use App\Controller\ErrorController;
17 | use Cake\TestSuite\TestCase;
18 |
19 | /**
20 | * {@see \App\Controller\ErrorController} Test Case
21 | *
22 | * @coversDefaultClass \App\Controller\ErrorController
23 | * @uses \App\Controller\ErrorController
24 | */
25 | class ErrorControllerTest extends TestCase
26 | {
27 | /**
28 | * Test subject
29 | *
30 | * @var \App\Controller\ErrorController
31 | */
32 | protected $ErrorController;
33 |
34 | /**
35 | * Setup controller to test with request config
36 | *
37 | * @return void
38 | */
39 | protected function setupController(): void
40 | {
41 | $this->ErrorController = new ErrorController();
42 | }
43 |
44 | /**
45 | * test `initialize` function
46 | *
47 | * @covers ::initialize()
48 | * @return void
49 | */
50 | public function testInitialize(): void
51 | {
52 | $this->setupController();
53 |
54 | static::assertNotEmpty($this->ErrorController->{'RequestHandler'});
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Middleware/ConfigurationMiddleware.php:
--------------------------------------------------------------------------------
1 | for more details.
12 | */
13 | namespace App\Middleware;
14 |
15 | use App\Utility\ApiConfigTrait;
16 | use BEdita\WebTools\ApiClientProvider;
17 | use Cake\Core\Configure;
18 | use Psr\Http\Message\ResponseInterface;
19 | use Psr\Http\Message\ServerRequestInterface;
20 | use Psr\Http\Server\MiddlewareInterface;
21 | use Psr\Http\Server\RequestHandlerInterface;
22 |
23 | /**
24 | * Configuration middleware.
25 | *
26 | * Load configuration from API using cache.
27 | */
28 | class ConfigurationMiddleware implements MiddlewareInterface
29 | {
30 | use ApiConfigTrait;
31 |
32 | /**
33 | * @inheritDoc
34 | */
35 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
36 | {
37 | if (Configure::check('API.apiBaseUrl')) {
38 | $identity = $request->getAttribute('identity');
39 | if ($identity && $identity->get('tokens')) {
40 | ApiClientProvider::getApiClient()->setupTokens($identity->get('tokens'));
41 | }
42 | $this->readApiConfig();
43 | }
44 |
45 | return $handler->handle($request);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/resources/js/app/components/form/field-radio.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
16 | {{ item }}
17 |
18 |
19 |
20 |
62 |
72 |
--------------------------------------------------------------------------------
/resources/js/app/components/object-nav/object-nav.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
17 |
{{ index }} / {{ total }}
18 |
19 |
20 |
21 |
58 |
--------------------------------------------------------------------------------
/resources/js/app/components/form/field-textarea.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
64 |
--------------------------------------------------------------------------------
/templates/Element/Model/sidebar_links.twig:
--------------------------------------------------------------------------------
1 | {% if in_array(resourceType, ['object_types', 'relations']) %}
2 | {% do _view.append(
3 | 'app-module-buttons',
4 | '' ~ __('Create') ~ ' '
5 | ) %}
6 | {% endif %}
7 |
8 | {% do _view.append(
9 | 'app-module-buttons',
10 | '' ~ __('Export') ~ ' '
11 | ) %}
12 |
13 | {# append links to sidebar #}
14 | {% do _view.append(
15 | 'app-module-links',
16 | '' ~ __('Object types') ~ ' '
17 | ~ '' ~ __('Relations') ~ ' '
18 | ~ '' ~ __('Property types') ~ ' '
19 | ~ '' ~ __('Categories') ~ ' '
20 | ~ '' ~ __('Tags') ~ ' '
21 | ) %}
22 |
--------------------------------------------------------------------------------
/resources/js/app/components/show-hide/show-hide.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ msgShow }}
6 |
7 |
8 |
9 | {{ msgHide }}
10 |
11 |
12 |
13 |
53 |
61 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG PHP_VERSION=8.3
2 | FROM chialab/php:${PHP_VERSION}-apache
3 |
4 | # Install Wait-for-it and configure PHP
5 | RUN curl -o /wait-for-it.sh https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh \
6 | && chmod +x /wait-for-it.sh \
7 | && echo "[PHP]\noutput_buffering = 4096\nmemory_limit = -1" > /usr/local/etc/php/php.ini
8 |
9 | # Copy files
10 | COPY . /var/www/html
11 |
12 | # Set APP_NAME to avoid .env load
13 | ENV APP_NAME BE-MANAGER
14 | ARG DEBUG
15 | ENV DEBUG ${DEBUG:-false}
16 |
17 | # Install libraries
18 | WORKDIR /var/www/html
19 | RUN chown -R www-data:www-data /var/www/html
20 | USER www-data:www-data
21 |
22 | RUN if [ ! "$DEBUG" = "true" ]; then export COMPOSER_ARGS='--no-dev'; fi \
23 | && composer install $COMPOSER_ARGS --optimize-autoloader --no-interaction --quiet
24 |
25 | # Restore user `root` to install node & yarn and to make sure we can bind to address 0.0.0.0:80
26 | USER root:root
27 |
28 | # Install node and yarn
29 | ENV NODE_VERSION=22.11.0
30 | RUN apt install -y curl
31 | RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.2/install.sh | bash
32 | ENV NVM_DIR=/root/.nvm
33 | RUN . "$NVM_DIR/nvm.sh" && nvm install ${NODE_VERSION}
34 | RUN . "$NVM_DIR/nvm.sh" && nvm use v${NODE_VERSION}
35 | RUN . "$NVM_DIR/nvm.sh" && nvm alias default v${NODE_VERSION}
36 | ENV PATH="/root/.nvm/versions/node/v${NODE_VERSION}/bin/:${PATH}"
37 | RUN npm install -g yarn
38 | RUN yarn && yarn build
39 |
40 | ENV LOG_DEBUG_URL="console:///?stream=php://stdout" \
41 | LOG_ERROR_URL="console:///?stream=php://stderr"
42 |
43 | HEALTHCHECK --interval=30s --timeout=3s --start-period=1m \
44 | CMD curl -f http://localhost/login || exit 1
45 |
46 | CMD ["apache2-foreground"]
47 |
--------------------------------------------------------------------------------
/tests/TestCase/Controller/MultiuploadControllerTest.php:
--------------------------------------------------------------------------------
1 | for more details.
13 | */
14 |
15 | namespace App\Test\TestCase\Controller;
16 |
17 | use App\Controller\MultiuploadController;
18 | use Cake\Http\ServerRequest;
19 |
20 | /**
21 | * {@see \App\Controller\MultiuploadController} Test Case
22 | *
23 | * @coversDefaultClass \App\Controller\MultiuploadController
24 | */
25 | class MultiuploadControllerTest extends BaseControllerTest
26 | {
27 | /**
28 | * Test `initialize` method
29 | *
30 | * @return void
31 | * @covers ::initialize()
32 | */
33 | public function testInitialize(): void
34 | {
35 | $config = [
36 | 'params' => [
37 | 'object_type' => 'documents',
38 | ],
39 | ];
40 | $request = new ServerRequest($config);
41 | $controller = new MultiuploadController($request);
42 | $viewVars = $controller->viewBuilder()->getVars();
43 | $expected = ['currentModule', 'objectType'];
44 | foreach ($expected as $varName) {
45 | static::assertArrayHasKey($varName, $viewVars);
46 | }
47 | static::assertSame('documents', $viewVars['currentModule']['name']);
48 | static::assertSame('documents', $viewVars['objectType']);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/templates/Element/Form/map.twig:
--------------------------------------------------------------------------------
1 | {% if object.attributes.coords %}
2 | {% set coords = object.attributes.coords|replace({
3 | 'POINT(': '',
4 | ')': '',
5 | })|split(' ') %}
6 |
7 | {% if coords[0] and coords[1] %}
8 |
11 |
12 |
13 |
16 | {{ __('Map') }}
17 |
18 |
19 |
20 | {% if not config('Maps.mapbox.token') %}
21 |
22 |
{{ __('Please provide a Mapbox access token in app configuration') }}.
23 |
24 | {% else %}
25 |
26 | {% set popup = object.attributes.address | default(object.attributes.locality) %}
27 | {% if not popup %}
28 | {% set popup = object.attributes.title %}
29 | {% endif %}
30 |
31 |
36 |
37 | {% endif %}
38 |
39 |
40 |
41 |
42 |
43 | {% endif %}
44 |
45 | {% endif %}
46 |
--------------------------------------------------------------------------------
/config/oembed.php:
--------------------------------------------------------------------------------
1 | [
15 | 'providers' => [
16 | 'soundcloud' => [
17 | 'name' => 'SoundCloud',
18 | 'match' => ['soundcloud.com'],
19 | 'url' => 'https://soundcloud.com/oembed',
20 | ],
21 | 'spotify' => [
22 | 'name' => 'Spotify',
23 | 'match' => ['*.spotify.com'],
24 | 'url' => 'https://embed.spotify.com/oembed',
25 | ],
26 | 'vimeo' => [
27 | 'name' => 'Vimeo',
28 | 'match' => [
29 | 'vimeo.com',
30 | '*.vimeo.com',
31 | ],
32 | 'url' => 'https://vimeo.com/api/oembed.json',
33 | 'options' => [
34 | 'headers' => [
35 | 'Referer' => env('VIMEO_REFERER_HEADER', Router::url('/', true)),
36 | ],
37 | ],
38 | ],
39 | 'youtube' => [
40 | 'name' => 'YouTube',
41 | 'match' => [
42 | 'youtube.com',
43 | 'www.youtube.com',
44 | 'youtu.be',
45 | ],
46 | 'url' => 'https://www.youtube.com/oembed',
47 | ],
48 | ],
49 | ],
50 | ];
51 |
--------------------------------------------------------------------------------
/resources/js/libs/urlUtils.js:
--------------------------------------------------------------------------------
1 | const setSearchParam = (name, value, searchParams) => {
2 | if (value === null) { // typeof null === 'object', because JavaScript
3 | return;
4 | }
5 |
6 | switch (typeof value) {
7 | case 'bigint':
8 | case 'number':
9 | case 'string':
10 | searchParams.set(name, value);
11 |
12 | return;
13 |
14 | case 'boolean':
15 | searchParams.set(name, value + 0);
16 |
17 | return;
18 |
19 | case 'undefined':
20 | case 'function':
21 | case 'symbol':
22 | return;
23 | }
24 |
25 | if (Array.isArray(value)) {
26 | for (let i = 0; i < value.length; i++) {
27 | setSearchParam(`${name}[${i}]`, value[i], searchParams,);
28 | }
29 |
30 | return;
31 | }
32 |
33 | for (let i in value) {
34 | if (typeof i !== 'string') {
35 | continue;
36 | }
37 |
38 | setSearchParam(`${name}[${encodeURIComponent(i)}]`, value[i], searchParams);
39 | }
40 | };
41 |
42 | /**
43 | * Build search params starting from an object and an initial URLSearchParams object
44 | *
45 | * @param {Object} object The object to process to build search params
46 | * @param {URLSearchParams} searchParams Initial search params
47 | * @returns {URLSearchParams}
48 | */
49 | const buildSearchParams = (object, searchParams = undefined) => {
50 | if (searchParams !== undefined) {
51 | searchParams = new URLSearchParams();
52 | }
53 | for (let key in object) {
54 | if (typeof key !== 'string') {
55 | continue;
56 | }
57 |
58 | setSearchParam(encodeURIComponent(key), object[key], searchParams);
59 | }
60 |
61 | return searchParams;
62 | };
63 |
64 | export { buildSearchParams };
65 |
--------------------------------------------------------------------------------