Initial import of space_profiles module
This commit is contained in:
76
Events.php
Normal file
76
Events.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace humhub\modules\space_profiles;
|
||||
|
||||
use Yii;
|
||||
|
||||
class Events
|
||||
{
|
||||
public static function onSpaceMenuInit($event): void
|
||||
{
|
||||
$space = $event->sender->space ?? null;
|
||||
if ($space === null || !$space->moduleManager->isEnabled('space_profiles')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event->sender->addItem([
|
||||
'label' => Yii::t('SpaceProfilesModule.base', 'Rescue Profile'),
|
||||
'group' => 'modules',
|
||||
'url' => $space->createUrl('/space_profiles/profile/view'),
|
||||
'icon' => '<i class="fa fa-id-card"></i>',
|
||||
'sortOrder' => 90,
|
||||
'isActive' => (
|
||||
Yii::$app->controller
|
||||
&& Yii::$app->controller->module
|
||||
&& Yii::$app->controller->module->id === 'space_profiles'
|
||||
&& Yii::$app->controller->id === 'profile'
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function onRescueSettingsMenuInit($event): void
|
||||
{
|
||||
$space = $event->sender->space ?? null;
|
||||
if ($space === null || !$space->moduleManager->isEnabled('space_profiles')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event->sender->addItem([
|
||||
'label' => Yii::t('SpaceProfilesModule.base', 'Space Profile'),
|
||||
'url' => $space->createUrl('/space_profiles/settings'),
|
||||
'sortOrder' => 200,
|
||||
'isActive' => (
|
||||
Yii::$app->controller
|
||||
&& Yii::$app->controller->module
|
||||
&& Yii::$app->controller->module->id === 'space_profiles'
|
||||
&& Yii::$app->controller->id === 'settings'
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function onSpaceAdminMenuInitFallback($event): void
|
||||
{
|
||||
$space = $event->sender->space ?? null;
|
||||
if ($space === null || !$space->moduleManager->isEnabled('space_profiles')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($space->moduleManager->isEnabled('rescue_foundation') || !$space->isAdmin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event->sender->addItem([
|
||||
'label' => Yii::t('SpaceProfilesModule.base', 'Space Profile'),
|
||||
'group' => 'admin',
|
||||
'url' => $space->createUrl('/space_profiles/settings'),
|
||||
'icon' => '<i class="fa fa-id-card"></i>',
|
||||
'sortOrder' => 590,
|
||||
'isActive' => (
|
||||
Yii::$app->controller
|
||||
&& Yii::$app->controller->module
|
||||
&& Yii::$app->controller->module->id === 'space_profiles'
|
||||
&& Yii::$app->controller->id === 'settings'
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
26
Module.php
Normal file
26
Module.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace humhub\modules\space_profiles;
|
||||
|
||||
use humhub\modules\content\components\ContentContainerActiveRecord;
|
||||
use humhub\modules\content\components\ContentContainerModule;
|
||||
use humhub\modules\space\models\Space;
|
||||
|
||||
class Module extends ContentContainerModule
|
||||
{
|
||||
public $resourcesPath = 'resources';
|
||||
|
||||
public function getContentContainerTypes()
|
||||
{
|
||||
return [Space::class];
|
||||
}
|
||||
|
||||
public function getContentContainerConfigUrl(ContentContainerActiveRecord $container)
|
||||
{
|
||||
if ($container->moduleManager->isEnabled('rescue_foundation')) {
|
||||
return $container->createUrl('/rescue_foundation/settings');
|
||||
}
|
||||
|
||||
return $container->createUrl('/space_profiles/settings');
|
||||
}
|
||||
}
|
||||
27
README.md
Normal file
27
README.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Space Profiles Module
|
||||
|
||||
Template-driven rescue space profile pages with branding assets and controlled HTML regions for HumHub spaces.
|
||||
|
||||
## Requirements
|
||||
|
||||
- HumHub 1.14+
|
||||
- `rescue_foundation` module available in the same HumHub instance
|
||||
|
||||
## Installation
|
||||
|
||||
Clone into the HumHub modules path using the folder name `space_profiles`:
|
||||
|
||||
```bash
|
||||
git clone https://gitea.kelinreij.duckdns.org/humhub-modules/space-profiles.git /var/www/localhost/htdocs/protected/modules/space_profiles
|
||||
```
|
||||
|
||||
Enable the module in HumHub (Marketplace / Modules UI), then run migrations:
|
||||
|
||||
```bash
|
||||
php /var/www/localhost/htdocs/protected/yii migrate/up --include-module-migrations=1 --interactive=0
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- This module is designed for space-level use.
|
||||
- If you run in Docker, execute commands inside the HumHub app container.
|
||||
26
components/TemplateRegistry.php
Normal file
26
components/TemplateRegistry.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace humhub\modules\space_profiles\components;
|
||||
|
||||
use Yii;
|
||||
|
||||
class TemplateRegistry
|
||||
{
|
||||
public const TEMPLATE_RESCUE_CENTER = 'rescue_center';
|
||||
|
||||
public static function options(): array
|
||||
{
|
||||
return [
|
||||
self::TEMPLATE_RESCUE_CENTER => Yii::t('SpaceProfilesModule.base', 'Rescue Center (Default)'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function resolveView(?string $templateKey): string
|
||||
{
|
||||
if ($templateKey === self::TEMPLATE_RESCUE_CENTER || empty($templateKey)) {
|
||||
return 'templates/rescue-center';
|
||||
}
|
||||
|
||||
return 'templates/rescue-center';
|
||||
}
|
||||
}
|
||||
74
components/UrlRule.php
Normal file
74
components/UrlRule.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace humhub\modules\space_profiles\components;
|
||||
|
||||
use humhub\components\ContentContainerUrlRuleInterface;
|
||||
use humhub\modules\content\components\ContentContainerActiveRecord;
|
||||
use yii\base\Component;
|
||||
use yii\web\UrlManager;
|
||||
use yii\web\UrlRuleInterface;
|
||||
|
||||
class UrlRule extends Component implements UrlRuleInterface, ContentContainerUrlRuleInterface
|
||||
{
|
||||
public const CONTAINER_ROUTE = 'space_profiles/profile/view';
|
||||
public const PUBLIC_ROUTE = 'space_profiles/public/view';
|
||||
private const ROUTE_SEGMENT = 'rescues';
|
||||
|
||||
public function parseContentContainerRequest(ContentContainerActiveRecord $container, UrlManager $manager, string $containerUrlPath, array $urlParams)
|
||||
{
|
||||
if ($containerUrlPath === self::ROUTE_SEGMENT) {
|
||||
return [self::CONTAINER_ROUTE, $urlParams];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function createContentContainerUrl(UrlManager $manager, string $containerUrlPath, string $route, array $params)
|
||||
{
|
||||
if ($route !== self::CONTAINER_ROUTE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$url = $containerUrlPath . '/' . self::ROUTE_SEGMENT;
|
||||
if (!empty($params)) {
|
||||
$query = http_build_query($params);
|
||||
if ($query !== '') {
|
||||
$url .= '?' . $query;
|
||||
}
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
public function createUrl($manager, $route, $params)
|
||||
{
|
||||
if ($route !== self::PUBLIC_ROUTE || empty($params['slug'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$slug = urlencode((string)$params['slug']);
|
||||
unset($params['slug']);
|
||||
|
||||
$url = self::ROUTE_SEGMENT . '/' . $slug;
|
||||
if (!empty($params) && ($query = http_build_query($params)) !== '') {
|
||||
$url .= '?' . $query;
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
public function parseRequest($manager, $request)
|
||||
{
|
||||
$path = trim($request->getPathInfo(), '/');
|
||||
if (strpos($path, self::ROUTE_SEGMENT . '/') !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$parts = explode('/', $path);
|
||||
if (empty($parts[1])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return [self::PUBLIC_ROUTE, ['slug' => urldecode($parts[1])]];
|
||||
}
|
||||
}
|
||||
19
config.php
Normal file
19
config.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
use humhub\modules\space\widgets\HeaderControlsMenu;
|
||||
use humhub\modules\space\widgets\Menu;
|
||||
use humhub\modules\space_profiles\Events;
|
||||
|
||||
return [
|
||||
'id' => 'space_profiles',
|
||||
'class' => 'humhub\\modules\\space_profiles\\Module',
|
||||
'namespace' => 'humhub\\modules\\space_profiles',
|
||||
'urlManagerRules' => [
|
||||
['class' => 'humhub\\modules\\space_profiles\\components\\UrlRule'],
|
||||
],
|
||||
'events' => [
|
||||
[Menu::class, Menu::EVENT_INIT, [Events::class, 'onSpaceMenuInit']],
|
||||
['humhub\\modules\\rescue_foundation\\widgets\\RescueSettingsMenu', 'rescueSettingsMenuInit', [Events::class, 'onRescueSettingsMenuInit']],
|
||||
[HeaderControlsMenu::class, HeaderControlsMenu::EVENT_INIT, [Events::class, 'onSpaceAdminMenuInitFallback']],
|
||||
],
|
||||
];
|
||||
25
controllers/ProfileController.php
Normal file
25
controllers/ProfileController.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace humhub\modules\space_profiles\controllers;
|
||||
|
||||
use humhub\modules\content\components\ContentContainerController;
|
||||
use humhub\modules\space_profiles\models\SpaceProfile;
|
||||
|
||||
class ProfileController extends ContentContainerController
|
||||
{
|
||||
protected function getAccessRules()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function actionView()
|
||||
{
|
||||
$profile = SpaceProfile::findOne(['contentcontainer_id' => $this->contentContainer->contentcontainer_id]);
|
||||
|
||||
return $this->render('view', [
|
||||
'space' => $this->contentContainer,
|
||||
'profile' => $profile,
|
||||
'isPublicRoute' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
43
controllers/PublicController.php
Normal file
43
controllers/PublicController.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace humhub\modules\space_profiles\controllers;
|
||||
|
||||
use humhub\components\Controller;
|
||||
use humhub\modules\space\models\Space;
|
||||
use humhub\modules\space_profiles\helpers\ProfileAccess;
|
||||
use humhub\modules\space_profiles\models\SpaceProfile;
|
||||
use Yii;
|
||||
use yii\web\ForbiddenHttpException;
|
||||
use yii\web\NotFoundHttpException;
|
||||
|
||||
class PublicController extends Controller
|
||||
{
|
||||
public function actionView(string $slug)
|
||||
{
|
||||
$profile = SpaceProfile::findOne(['slug' => $slug]);
|
||||
if (!$profile instanceof SpaceProfile) {
|
||||
throw new NotFoundHttpException('Profile not found.');
|
||||
}
|
||||
|
||||
$space = Space::findOne(['contentcontainer_id' => $profile->contentcontainer_id]);
|
||||
if (!$space instanceof Space) {
|
||||
throw new NotFoundHttpException('Space not found.');
|
||||
}
|
||||
|
||||
if (!$space->moduleManager->isEnabled('space_profiles')) {
|
||||
throw new NotFoundHttpException('Profile not available.');
|
||||
}
|
||||
|
||||
if (!ProfileAccess::canView($space)) {
|
||||
throw new ForbiddenHttpException('You are not allowed to view this profile.');
|
||||
}
|
||||
|
||||
$this->view->title = Yii::t('SpaceProfilesModule.base', 'Rescue Profile') . ' - ' . $space->name;
|
||||
|
||||
return $this->render('view', [
|
||||
'space' => $space,
|
||||
'profile' => $profile,
|
||||
'isPublicRoute' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
44
controllers/SettingsController.php
Normal file
44
controllers/SettingsController.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace humhub\modules\space_profiles\controllers;
|
||||
|
||||
use humhub\modules\content\components\ContentContainerController;
|
||||
use humhub\modules\content\components\ContentContainerControllerAccess;
|
||||
use humhub\modules\rescue_foundation\widgets\RescueSettingsMenu;
|
||||
use humhub\modules\space\models\Space;
|
||||
use humhub\modules\space_profiles\models\forms\SpaceProfileForm;
|
||||
use Yii;
|
||||
use yii\web\UploadedFile;
|
||||
|
||||
class SettingsController extends ContentContainerController
|
||||
{
|
||||
protected function getAccessRules()
|
||||
{
|
||||
return [[ContentContainerControllerAccess::RULE_USER_GROUP_ONLY => [Space::USERGROUP_OWNER, Space::USERGROUP_ADMIN]]];
|
||||
}
|
||||
|
||||
public function actionIndex()
|
||||
{
|
||||
$model = new SpaceProfileForm(['contentContainer' => $this->contentContainer]);
|
||||
|
||||
if ($model->load(Yii::$app->request->post())) {
|
||||
$model->iconFile = UploadedFile::getInstance($model, 'iconFile');
|
||||
$model->backgroundImageFile = UploadedFile::getInstance($model, 'backgroundImageFile');
|
||||
|
||||
if ($model->save()) {
|
||||
$this->view->saved();
|
||||
return $this->refresh();
|
||||
}
|
||||
}
|
||||
|
||||
$subNav = null;
|
||||
if (class_exists(RescueSettingsMenu::class)) {
|
||||
$subNav = RescueSettingsMenu::widget(['space' => $this->contentContainer]);
|
||||
}
|
||||
|
||||
return $this->render('index', [
|
||||
'model' => $model,
|
||||
'subNav' => $subNav,
|
||||
]);
|
||||
}
|
||||
}
|
||||
37
helpers/ProfileAccess.php
Normal file
37
helpers/ProfileAccess.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace humhub\modules\space_profiles\helpers;
|
||||
|
||||
use humhub\modules\space\models\Space;
|
||||
use humhub\modules\user\models\User;
|
||||
use Yii;
|
||||
|
||||
class ProfileAccess
|
||||
{
|
||||
public static function canView(Space $space, User $user = null): bool
|
||||
{
|
||||
if ($space->status === Space::STATUS_DISABLED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$user = $user ?: (!Yii::$app->user->isGuest ? Yii::$app->user->getIdentity() : null);
|
||||
|
||||
if ($user && method_exists($user, 'isSystemAdmin') && $user->isSystemAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($user && method_exists($space, 'isMember') && $space->isMember($user)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($space->visibility === Space::VISIBILITY_ALL) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($space->visibility === Space::VISIBILITY_REGISTERED_ONLY) {
|
||||
return $user !== null;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
101
helpers/ProfileHtmlSanitizer.php
Normal file
101
helpers/ProfileHtmlSanitizer.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace humhub\modules\space_profiles\helpers;
|
||||
|
||||
use yii\helpers\HtmlPurifier;
|
||||
|
||||
class ProfileHtmlSanitizer
|
||||
{
|
||||
public static function sanitize(?string $html): string
|
||||
{
|
||||
$value = (string)$html;
|
||||
if ($value === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$styleBlocks = [];
|
||||
if (preg_match_all('#<style\\b[^>]*>(.*?)</style>#is', $value, $matches)) {
|
||||
$styleBlocks = $matches[1] ?? [];
|
||||
}
|
||||
$value = preg_replace('#<style\\b[^>]*>.*?</style>#is', '', $value) ?? '';
|
||||
|
||||
$value = preg_replace('#<script\\b[^>]*>.*?</script>#is', '', $value) ?? '';
|
||||
$value = preg_replace("/\\son[a-z]+\\s*=\\s*(\"[^\"]*\"|'[^']*'|[^\\s>]+)/i", '', $value) ?? '';
|
||||
$value = preg_replace('/javascript\\s*:/i', '', $value) ?? '';
|
||||
$value = preg_replace('/expression\\s*\\(/i', '', $value) ?? '';
|
||||
|
||||
$purified = HtmlPurifier::process($value, [
|
||||
'HTML.Allowed' => 'div,p,br,span,strong,em,b,i,u,ul,ol,li,h1,h2,h3,h4,h5,h6,a[href|title|target],img[src|alt|title|width|height],blockquote,hr,table,thead,tbody,tr,th,td,code,pre',
|
||||
'Attr.EnableID' => false,
|
||||
'CSS.Trusted' => false,
|
||||
'URI.AllowedSchemes' => [
|
||||
'http' => true,
|
||||
'https' => true,
|
||||
'mailto' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$scopedCss = [];
|
||||
foreach ($styleBlocks as $styleBlock) {
|
||||
$css = self::scopeCss((string)$styleBlock);
|
||||
if (trim($css) !== '') {
|
||||
$scopedCss[] = $css;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($scopedCss)) {
|
||||
return $purified;
|
||||
}
|
||||
|
||||
return $purified . "\n<style>\n" . implode("\n", $scopedCss) . "\n</style>";
|
||||
}
|
||||
|
||||
private static function scopeCss(string $css): string
|
||||
{
|
||||
$css = preg_replace('/@import\\s+[^;]+;/i', '', $css) ?? '';
|
||||
$css = preg_replace('/javascript\\s*:/i', '', $css) ?? '';
|
||||
$css = preg_replace('/expression\\s*\\(/i', '', $css) ?? '';
|
||||
|
||||
$chunks = explode('}', $css);
|
||||
$scoped = [];
|
||||
|
||||
foreach ($chunks as $chunk) {
|
||||
if (trim($chunk) === '' || strpos($chunk, '{') === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
[$selectors, $body] = explode('{', $chunk, 2);
|
||||
$selectors = trim($selectors);
|
||||
if ($selectors === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strpos($selectors, '@') === 0) {
|
||||
$scoped[] = $selectors . '{' . $body . '}';
|
||||
continue;
|
||||
}
|
||||
|
||||
$selectorParts = array_filter(array_map('trim', explode(',', $selectors)));
|
||||
$scopedSelectors = [];
|
||||
|
||||
foreach ($selectorParts as $selector) {
|
||||
$selector = str_replace(':root', '.rescue-profile-scope', $selector);
|
||||
if (preg_match('/^(html|body)\\b/i', $selector)) {
|
||||
$selector = preg_replace('/^(html|body)\\b/i', '.rescue-profile-scope', $selector) ?? '.rescue-profile-scope';
|
||||
}
|
||||
|
||||
if (strpos($selector, '.rescue-profile-scope') !== 0) {
|
||||
$selector = '.rescue-profile-scope ' . $selector;
|
||||
}
|
||||
|
||||
$scopedSelectors[] = $selector;
|
||||
}
|
||||
|
||||
if (!empty($scopedSelectors)) {
|
||||
$scoped[] = implode(', ', $scopedSelectors) . '{' . $body . '}';
|
||||
}
|
||||
}
|
||||
|
||||
return implode("\\n", $scoped);
|
||||
}
|
||||
}
|
||||
104
migrations/m260401_130000_initial.php
Normal file
104
migrations/m260401_130000_initial.php
Normal file
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
use humhub\components\Migration;
|
||||
|
||||
class m260401_130000_initial extends Migration
|
||||
{
|
||||
public function safeUp()
|
||||
{
|
||||
$this->safeCreateTable('rescue_space_profile', [
|
||||
'id' => $this->primaryKey(),
|
||||
'contentcontainer_id' => $this->integer()->notNull(),
|
||||
'rescue_name' => $this->string(190)->notNull(),
|
||||
'address' => $this->string(255)->notNull(),
|
||||
'city' => $this->string(120)->notNull(),
|
||||
'state' => $this->string(2)->notNull(),
|
||||
'zip' => $this->string(10)->notNull(),
|
||||
'email' => $this->string(190)->notNull(),
|
||||
'phone' => $this->string(32)->notNull(),
|
||||
'animals_we_accept' => $this->text()->notNull(),
|
||||
'description' => $this->text()->notNull(),
|
||||
'mission_statement' => $this->text()->notNull(),
|
||||
'template_key' => $this->string(64)->defaultValue('rescue_center')->notNull(),
|
||||
'header_html' => $this->text()->null(),
|
||||
'body_html' => $this->text()->null(),
|
||||
'footer_html' => $this->text()->null(),
|
||||
'icon_path' => $this->string(255)->null(),
|
||||
'background_image_path' => $this->string(255)->null(),
|
||||
'created_at' => $this->dateTime()->null(),
|
||||
'updated_at' => $this->dateTime()->null(),
|
||||
]);
|
||||
|
||||
$this->safeCreateIndex('idx_rescue_space_profile_container', 'rescue_space_profile', 'contentcontainer_id', true);
|
||||
$this->safeAddForeignKey(
|
||||
'fk_rescue_space_profile_container',
|
||||
'rescue_space_profile',
|
||||
'contentcontainer_id',
|
||||
'contentcontainer',
|
||||
'id',
|
||||
'CASCADE'
|
||||
);
|
||||
|
||||
$this->seedFieldMetadata();
|
||||
}
|
||||
|
||||
public function safeDown()
|
||||
{
|
||||
$this->safeDropTable('rescue_space_profile');
|
||||
}
|
||||
|
||||
private function seedFieldMetadata(): void
|
||||
{
|
||||
if ($this->db->getSchema()->getTableSchema('rescue_field_definition', true) === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$createdAt = date('Y-m-d H:i:s');
|
||||
$rows = [
|
||||
['rescue_name', 'Rescue name', 'text', 1, 1, 110],
|
||||
['address', 'Address', 'text', 1, 1, 120],
|
||||
['city', 'City', 'text', 1, 1, 130],
|
||||
['state', 'State', 'text', 1, 1, 140],
|
||||
['zip', 'ZIP', 'text', 1, 1, 150],
|
||||
['email', 'Email', 'email', 1, 1, 160],
|
||||
['phone', 'Phone', 'text', 1, 1, 170],
|
||||
['animals_we_accept', 'Animals we accept', 'textarea', 1, 1, 180],
|
||||
['description', 'Description', 'textarea', 1, 1, 190],
|
||||
['mission_statement', 'Mission statement', 'textarea', 1, 1, 200],
|
||||
['template_key', 'Template', 'select', 1, 1, 205],
|
||||
['header_html', 'Header HTML', 'html', 0, 1, 210],
|
||||
['body_html', 'Body HTML', 'html', 0, 1, 220],
|
||||
['footer_html', 'Footer HTML', 'html', 0, 1, 230],
|
||||
['icon_path', 'Icon', 'image', 0, 1, 240],
|
||||
['background_image_path', 'Background image', 'image', 0, 1, 250],
|
||||
];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
[$fieldKey, $label, $inputType, $required, $isCore, $sortOrder] = $row;
|
||||
$exists = (new \yii\db\Query())
|
||||
->from('rescue_field_definition')
|
||||
->where(['module_id' => 'space_profiles', 'field_key' => $fieldKey])
|
||||
->exists($this->db);
|
||||
|
||||
if ($exists) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->insert('rescue_field_definition', [
|
||||
'module_id' => 'space_profiles',
|
||||
'group_key' => 'space_profile',
|
||||
'field_key' => $fieldKey,
|
||||
'label' => $label,
|
||||
'input_type' => $inputType,
|
||||
'required' => $required,
|
||||
'is_core' => $isCore,
|
||||
'is_active' => 1,
|
||||
'visibility' => 'public',
|
||||
'options' => '{}',
|
||||
'sort_order' => $sortOrder,
|
||||
'created_at' => $createdAt,
|
||||
'updated_at' => $createdAt,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
26
migrations/m260401_141000_add_template_key.php
Normal file
26
migrations/m260401_141000_add_template_key.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use humhub\components\Migration;
|
||||
|
||||
class m260401_141000_add_template_key extends Migration
|
||||
{
|
||||
public function safeUp()
|
||||
{
|
||||
$schema = $this->db->getSchema()->getTableSchema('rescue_space_profile', true);
|
||||
if ($schema === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isset($schema->columns['template_key'])) {
|
||||
$this->addColumn('rescue_space_profile', 'template_key', $this->string(64)->defaultValue('rescue_center')->notNull());
|
||||
}
|
||||
}
|
||||
|
||||
public function safeDown()
|
||||
{
|
||||
$schema = $this->db->getSchema()->getTableSchema('rescue_space_profile', true);
|
||||
if ($schema !== null && isset($schema->columns['template_key'])) {
|
||||
$this->dropColumn('rescue_space_profile', 'template_key');
|
||||
}
|
||||
}
|
||||
}
|
||||
92
migrations/m260401_200000_add_slug.php
Normal file
92
migrations/m260401_200000_add_slug.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
use humhub\components\Migration;
|
||||
|
||||
class m260401_200000_add_slug extends Migration
|
||||
{
|
||||
public function safeUp()
|
||||
{
|
||||
$schema = $this->db->getSchema()->getTableSchema('rescue_space_profile', true);
|
||||
if ($schema === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isset($schema->columns['slug'])) {
|
||||
$this->addColumn('rescue_space_profile', 'slug', $this->string(190)->null()->after('mission_statement'));
|
||||
}
|
||||
|
||||
$rows = (new \yii\db\Query())
|
||||
->select([
|
||||
'id' => 'rsp.id',
|
||||
'rescue_name' => 'rsp.rescue_name',
|
||||
'space_name' => 's.name',
|
||||
])
|
||||
->from(['rsp' => 'rescue_space_profile'])
|
||||
->leftJoin(['cc' => 'contentcontainer'], 'cc.id = rsp.contentcontainer_id')
|
||||
->leftJoin(['s' => 'space'], 's.contentcontainer_id = cc.id')
|
||||
->orderBy(['rsp.id' => SORT_ASC])
|
||||
->all($this->db);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$source = trim((string)($row['space_name'] ?? ''));
|
||||
if ($source === '') {
|
||||
$source = trim((string)($row['rescue_name'] ?? ''));
|
||||
}
|
||||
|
||||
$slug = $this->createUniqueSlug($source, (int)$row['id']);
|
||||
$this->update('rescue_space_profile', ['slug' => $slug], ['id' => (int)$row['id']]);
|
||||
}
|
||||
|
||||
$this->alterColumn('rescue_space_profile', 'slug', $this->string(190)->notNull());
|
||||
$this->safeCreateIndex('ux_rescue_space_profile_slug', 'rescue_space_profile', 'slug', true);
|
||||
}
|
||||
|
||||
public function safeDown()
|
||||
{
|
||||
$schema = $this->db->getSchema()->getTableSchema('rescue_space_profile', true);
|
||||
if ($schema === null || !isset($schema->columns['slug'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->safeDropIndex('ux_rescue_space_profile_slug', 'rescue_space_profile');
|
||||
$this->dropColumn('rescue_space_profile', 'slug');
|
||||
}
|
||||
|
||||
private function createUniqueSlug(string $source, int $rowId): string
|
||||
{
|
||||
$base = $this->normalizeToSlug($source);
|
||||
$candidate = $base;
|
||||
$counter = 1;
|
||||
|
||||
while ($this->slugExists($candidate, $rowId)) {
|
||||
$candidate = $base . '-' . $counter;
|
||||
$counter++;
|
||||
}
|
||||
|
||||
return $candidate;
|
||||
}
|
||||
|
||||
private function normalizeToSlug(string $value): string
|
||||
{
|
||||
$normalized = trim($value);
|
||||
$ascii = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $normalized);
|
||||
if ($ascii !== false) {
|
||||
$normalized = $ascii;
|
||||
}
|
||||
|
||||
$slug = strtolower($normalized);
|
||||
$slug = preg_replace('/[^a-z0-9]+/', '-', $slug) ?? '';
|
||||
$slug = trim($slug, '-');
|
||||
|
||||
return $slug !== '' ? $slug : 'rescue';
|
||||
}
|
||||
|
||||
private function slugExists(string $slug, int $rowId): bool
|
||||
{
|
||||
return (new \yii\db\Query())
|
||||
->from('rescue_space_profile')
|
||||
->where(['slug' => $slug])
|
||||
->andWhere(['!=', 'id', $rowId])
|
||||
->exists($this->db);
|
||||
}
|
||||
}
|
||||
64
migrations/m260401_201000_set_rescues_default_home.php
Normal file
64
migrations/m260401_201000_set_rescues_default_home.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
use humhub\components\Migration;
|
||||
|
||||
class m260401_201000_set_rescues_default_home extends Migration
|
||||
{
|
||||
public function safeUp()
|
||||
{
|
||||
$rows = (new \yii\db\Query())
|
||||
->select(['contentcontainer_id', 'url', 'guid'])
|
||||
->from('space')
|
||||
->where(['in', 'contentcontainer_id', (new \yii\db\Query())
|
||||
->select('contentcontainer_id')
|
||||
->from('rescue_space_profile')])
|
||||
->all($this->db);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$spacePath = trim((string)($row['url'] ?? ''));
|
||||
if ($spacePath === '') {
|
||||
$spacePath = trim((string)($row['guid'] ?? ''));
|
||||
}
|
||||
|
||||
if ($spacePath === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$homeUrl = '/s/' . $spacePath . '/rescues';
|
||||
$contentContainerId = (int)$row['contentcontainer_id'];
|
||||
|
||||
$this->upsert('contentcontainer_setting', [
|
||||
'module_id' => 'space',
|
||||
'contentcontainer_id' => $contentContainerId,
|
||||
'name' => 'indexUrl',
|
||||
'value' => $homeUrl,
|
||||
], [
|
||||
'value' => $homeUrl,
|
||||
]);
|
||||
|
||||
$this->upsert('contentcontainer_setting', [
|
||||
'module_id' => 'space',
|
||||
'contentcontainer_id' => $contentContainerId,
|
||||
'name' => 'indexGuestUrl',
|
||||
'value' => $homeUrl,
|
||||
], [
|
||||
'value' => $homeUrl,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function safeDown()
|
||||
{
|
||||
$this->delete('contentcontainer_setting', [
|
||||
'and',
|
||||
['module_id' => 'space', 'name' => 'indexUrl'],
|
||||
['like', 'value', '/s/%/rescues', false],
|
||||
]);
|
||||
|
||||
$this->delete('contentcontainer_setting', [
|
||||
'and',
|
||||
['module_id' => 'space', 'name' => 'indexGuestUrl'],
|
||||
['like', 'value', '/s/%/rescues', false],
|
||||
]);
|
||||
}
|
||||
}
|
||||
33
models/SpaceProfile.php
Normal file
33
models/SpaceProfile.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace humhub\modules\space_profiles\models;
|
||||
|
||||
use humhub\components\ActiveRecord;
|
||||
use humhub\modules\space_profiles\components\TemplateRegistry;
|
||||
|
||||
class SpaceProfile extends ActiveRecord
|
||||
{
|
||||
public static function tableName()
|
||||
{
|
||||
return 'rescue_space_profile';
|
||||
}
|
||||
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
[['contentcontainer_id', 'rescue_name', 'address', 'city', 'state', 'zip', 'email', 'phone', 'animals_we_accept', 'description', 'mission_statement', 'template_key', 'slug'], 'required'],
|
||||
[['contentcontainer_id'], 'integer'],
|
||||
[['animals_we_accept', 'description', 'mission_statement', 'header_html', 'body_html', 'footer_html'], 'string'],
|
||||
[['template_key'], 'in', 'range' => array_keys(TemplateRegistry::options())],
|
||||
[['rescue_name', 'email', 'slug'], 'string', 'max' => 190],
|
||||
[['address', 'icon_path', 'background_image_path'], 'string', 'max' => 255],
|
||||
[['template_key'], 'string', 'max' => 64],
|
||||
[['city'], 'string', 'max' => 120],
|
||||
[['state'], 'string', 'max' => 2],
|
||||
[['zip'], 'string', 'max' => 10],
|
||||
[['phone'], 'string', 'max' => 32],
|
||||
[['contentcontainer_id'], 'unique'],
|
||||
[['slug'], 'unique'],
|
||||
];
|
||||
}
|
||||
}
|
||||
291
models/forms/SpaceProfileForm.php
Normal file
291
models/forms/SpaceProfileForm.php
Normal file
@@ -0,0 +1,291 @@
|
||||
<?php
|
||||
|
||||
namespace humhub\modules\space_profiles\models\forms;
|
||||
|
||||
use humhub\modules\content\components\ContentContainerActiveRecord;
|
||||
use humhub\modules\rescue_foundation\components\UploadStandards;
|
||||
use humhub\modules\rescue_foundation\components\ValidationStandards;
|
||||
use humhub\modules\space\models\Space;
|
||||
use humhub\modules\space_profiles\components\TemplateRegistry;
|
||||
use humhub\modules\space_profiles\helpers\ProfileHtmlSanitizer;
|
||||
use humhub\modules\space_profiles\models\SpaceProfile;
|
||||
use Yii;
|
||||
use yii\base\Model;
|
||||
use yii\helpers\FileHelper;
|
||||
use yii\web\UploadedFile;
|
||||
|
||||
class SpaceProfileForm extends Model
|
||||
{
|
||||
public ContentContainerActiveRecord $contentContainer;
|
||||
|
||||
public string $rescue_name = '';
|
||||
public string $address = '';
|
||||
public string $city = '';
|
||||
public string $state = '';
|
||||
public string $zip = '';
|
||||
public string $email = '';
|
||||
public string $phone = '';
|
||||
public string $animals_we_accept = '';
|
||||
public string $description = '';
|
||||
public string $mission_statement = '';
|
||||
public string $template_key = TemplateRegistry::TEMPLATE_RESCUE_CENTER;
|
||||
public ?string $header_html = null;
|
||||
public ?string $body_html = null;
|
||||
public ?string $footer_html = null;
|
||||
|
||||
public UploadedFile|string|null $iconFile = null;
|
||||
public UploadedFile|string|null $backgroundImageFile = null;
|
||||
public bool $removeIcon = false;
|
||||
public bool $removeBackgroundImage = false;
|
||||
|
||||
private ?SpaceProfile $profile = null;
|
||||
|
||||
public function init()
|
||||
{
|
||||
parent::init();
|
||||
$this->profile = SpaceProfile::findOne(['contentcontainer_id' => $this->contentContainer->contentcontainer_id]);
|
||||
|
||||
if ($this->profile) {
|
||||
$this->address = (string)$this->profile->address;
|
||||
$this->city = (string)$this->profile->city;
|
||||
$this->state = (string)$this->profile->state;
|
||||
$this->zip = (string)$this->profile->zip;
|
||||
$this->email = (string)$this->profile->email;
|
||||
$this->phone = (string)$this->profile->phone;
|
||||
$this->animals_we_accept = (string)$this->profile->animals_we_accept;
|
||||
$this->mission_statement = (string)$this->profile->mission_statement;
|
||||
$this->template_key = !empty($this->profile->template_key)
|
||||
? (string)$this->profile->template_key
|
||||
: TemplateRegistry::TEMPLATE_RESCUE_CENTER;
|
||||
$this->header_html = $this->profile->header_html;
|
||||
$this->body_html = $this->profile->body_html;
|
||||
$this->footer_html = $this->profile->footer_html;
|
||||
}
|
||||
|
||||
$this->rescue_name = $this->getSpaceName();
|
||||
$this->description = $this->getSpaceDescription();
|
||||
}
|
||||
|
||||
public function rules()
|
||||
{
|
||||
$contactRules = ValidationStandards::contactRules([
|
||||
'email' => 'email',
|
||||
'phone' => 'phone',
|
||||
'state' => 'state',
|
||||
'zip' => 'zip',
|
||||
]);
|
||||
|
||||
return array_merge([
|
||||
[['address', 'city', 'state', 'zip', 'email', 'phone', 'animals_we_accept', 'mission_statement', 'template_key'], 'required'],
|
||||
[['animals_we_accept', 'description', 'mission_statement', 'header_html', 'body_html', 'footer_html'], 'string'],
|
||||
[['template_key'], 'in', 'range' => array_keys(TemplateRegistry::options())],
|
||||
[['rescue_name', 'email'], 'string', 'max' => 190],
|
||||
[['address'], 'string', 'max' => 255],
|
||||
[['city'], 'string', 'max' => 120],
|
||||
[['state'], 'string', 'max' => 2],
|
||||
[['zip'], 'string', 'max' => 10],
|
||||
[['phone'], 'string', 'max' => 32],
|
||||
[['template_key'], 'string', 'max' => 64],
|
||||
[['removeIcon', 'removeBackgroundImage'], 'boolean'],
|
||||
[['iconFile', 'backgroundImageFile'], 'file',
|
||||
'skipOnEmpty' => true,
|
||||
'extensions' => UploadStandards::imageExtensions(),
|
||||
'checkExtensionByMimeType' => true,
|
||||
'mimeTypes' => UploadStandards::imageMimeTypes(),
|
||||
'maxSize' => UploadStandards::IMAGE_MAX_BYTES,
|
||||
],
|
||||
], $contactRules);
|
||||
}
|
||||
|
||||
public function save(): bool
|
||||
{
|
||||
if (!$this->validate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$persistedDescription = $this->resolvePersistedDescription();
|
||||
|
||||
$profile = $this->profile ?: new SpaceProfile();
|
||||
$profile->contentcontainer_id = $this->contentContainer->contentcontainer_id;
|
||||
$profile->rescue_name = $this->getSpaceName();
|
||||
$profile->address = trim($this->address);
|
||||
$profile->city = trim($this->city);
|
||||
$profile->state = strtoupper(trim($this->state));
|
||||
$profile->zip = trim($this->zip);
|
||||
$profile->email = trim($this->email);
|
||||
$profile->phone = trim($this->phone);
|
||||
$profile->animals_we_accept = trim($this->animals_we_accept);
|
||||
$profile->description = $persistedDescription;
|
||||
$profile->mission_statement = trim($this->mission_statement);
|
||||
$profile->slug = $this->generateUniqueSlug();
|
||||
$profile->template_key = $this->template_key;
|
||||
$profile->header_html = ProfileHtmlSanitizer::sanitize($this->header_html);
|
||||
$profile->body_html = ProfileHtmlSanitizer::sanitize($this->body_html);
|
||||
$profile->footer_html = ProfileHtmlSanitizer::sanitize($this->footer_html);
|
||||
|
||||
if ($this->removeIcon) {
|
||||
$this->deleteStoredFile($profile->icon_path);
|
||||
$profile->icon_path = null;
|
||||
}
|
||||
|
||||
if ($this->removeBackgroundImage) {
|
||||
$this->deleteStoredFile($profile->background_image_path);
|
||||
$profile->background_image_path = null;
|
||||
}
|
||||
|
||||
if ($this->iconFile instanceof UploadedFile) {
|
||||
$this->deleteStoredFile($profile->icon_path);
|
||||
$profile->icon_path = $this->storeImage($this->iconFile, 'icon');
|
||||
}
|
||||
|
||||
if ($this->backgroundImageFile instanceof UploadedFile) {
|
||||
$this->deleteStoredFile($profile->background_image_path);
|
||||
$profile->background_image_path = $this->storeImage($this->backgroundImageFile, 'background');
|
||||
}
|
||||
|
||||
if (!$profile->save()) {
|
||||
$this->addErrors($profile->getErrors());
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->setDefaultHomePage();
|
||||
$this->profile = $profile;
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getProfile(): ?SpaceProfile
|
||||
{
|
||||
return $this->profile;
|
||||
}
|
||||
|
||||
public function getTemplateOptions(): array
|
||||
{
|
||||
return TemplateRegistry::options();
|
||||
}
|
||||
|
||||
private function storeImage(UploadedFile $file, string $prefix): ?string
|
||||
{
|
||||
$relativeDir = '/uploads/space-profiles/' . $this->contentContainer->contentcontainer_id;
|
||||
$absoluteDir = Yii::getAlias('@webroot') . $relativeDir;
|
||||
FileHelper::createDirectory($absoluteDir);
|
||||
|
||||
$random = Yii::$app->security->generateRandomString(8);
|
||||
$fileName = $prefix . '-' . time() . '-' . $random . '.' . strtolower((string)$file->extension);
|
||||
$absolutePath = $absoluteDir . '/' . $fileName;
|
||||
|
||||
if (!$file->saveAs($absolutePath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $relativeDir . '/' . $fileName;
|
||||
}
|
||||
|
||||
private function deleteStoredFile(?string $relativePath): void
|
||||
{
|
||||
if (empty($relativePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$absolutePath = Yii::getAlias('@webroot') . $relativePath;
|
||||
if (is_file($absolutePath)) {
|
||||
@unlink($absolutePath);
|
||||
}
|
||||
}
|
||||
|
||||
private function buildBaseSlug(): string
|
||||
{
|
||||
$source = $this->getSpaceName();
|
||||
|
||||
$normalized = trim((string)$source);
|
||||
$ascii = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $normalized);
|
||||
if ($ascii !== false) {
|
||||
$normalized = $ascii;
|
||||
}
|
||||
|
||||
$slug = strtolower($normalized);
|
||||
$slug = preg_replace('/[^a-z0-9]+/', '-', $slug) ?? '';
|
||||
$slug = trim($slug, '-');
|
||||
|
||||
return $slug !== '' ? $slug : 'rescue';
|
||||
}
|
||||
|
||||
private function generateUniqueSlug(): string
|
||||
{
|
||||
$base = $this->buildBaseSlug();
|
||||
$candidate = $base;
|
||||
$suffix = 1;
|
||||
|
||||
while ($this->slugExists($candidate)) {
|
||||
$candidate = $base . '-' . $suffix;
|
||||
$suffix++;
|
||||
}
|
||||
|
||||
return $candidate;
|
||||
}
|
||||
|
||||
private function slugExists(string $slug): bool
|
||||
{
|
||||
$query = SpaceProfile::find()->where(['slug' => $slug]);
|
||||
|
||||
if ($this->profile && !$this->profile->isNewRecord) {
|
||||
$query->andWhere(['!=', 'id', (int)$this->profile->id]);
|
||||
}
|
||||
|
||||
return $query->exists();
|
||||
}
|
||||
|
||||
private function setDefaultHomePage(): void
|
||||
{
|
||||
if (!$this->contentContainer instanceof Space) {
|
||||
return;
|
||||
}
|
||||
|
||||
$defaultUrl = $this->contentContainer->createUrl('/space_profiles/profile/view');
|
||||
$settings = $this->contentContainer->getSettings();
|
||||
$settings->set('indexUrl', $defaultUrl);
|
||||
$settings->set('indexGuestUrl', $defaultUrl);
|
||||
}
|
||||
|
||||
private function getSpaceName(): string
|
||||
{
|
||||
if ($this->contentContainer instanceof Space) {
|
||||
return trim((string)$this->contentContainer->name);
|
||||
}
|
||||
|
||||
return trim($this->rescue_name);
|
||||
}
|
||||
|
||||
private function getSpaceDescription(): string
|
||||
{
|
||||
if ($this->contentContainer instanceof Space) {
|
||||
$about = trim((string)$this->contentContainer->about);
|
||||
if ($about !== '') {
|
||||
return $about;
|
||||
}
|
||||
|
||||
return trim((string)$this->contentContainer->description);
|
||||
}
|
||||
|
||||
return trim($this->description);
|
||||
}
|
||||
|
||||
private function resolvePersistedDescription(): string
|
||||
{
|
||||
$description = $this->getSpaceDescription();
|
||||
if ($description !== '') {
|
||||
return $description;
|
||||
}
|
||||
|
||||
$mission = trim($this->mission_statement);
|
||||
if ($mission !== '') {
|
||||
return $mission;
|
||||
}
|
||||
|
||||
$animals = trim($this->animals_we_accept);
|
||||
if ($animals !== '') {
|
||||
return $animals;
|
||||
}
|
||||
|
||||
return $this->getSpaceName();
|
||||
}
|
||||
}
|
||||
15
module.json
Normal file
15
module.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"id": "space_profiles",
|
||||
"name": "Space Profiles",
|
||||
"description": "Template-driven rescue space profile pages with branding and controlled HTML regions.",
|
||||
"keywords": ["space", "profile", "rescue", "branding"],
|
||||
"version": "0.1.0",
|
||||
"humhub": {
|
||||
"minVersion": "1.14"
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "Kelin Rescue Hub"
|
||||
}
|
||||
]
|
||||
}
|
||||
19
tests/README.md
Normal file
19
tests/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Space Profiles Test Notes
|
||||
|
||||
This module includes a unit suite scaffold under `tests/codeception/unit`.
|
||||
|
||||
Current unit coverage targets:
|
||||
- HTML sanitization hardening regressions
|
||||
- Public/profile access helper behavior
|
||||
- Form-level validation behavior for core profile fields
|
||||
|
||||
## Run
|
||||
|
||||
In a HumHub development environment with Codeception installed:
|
||||
|
||||
```bash
|
||||
cd protected
|
||||
php vendor/bin/codecept run -c modules/space_profiles/tests/codeception.yml unit
|
||||
```
|
||||
|
||||
If `vendor/bin/codecept` is not present, install dev dependencies for the HumHub environment first.
|
||||
18
tests/codeception.yml
Normal file
18
tests/codeception.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
actor: Tester
|
||||
namespace: space_profiles
|
||||
bootstrap: _bootstrap.php
|
||||
settings:
|
||||
suite_class: \PHPUnit_Framework_TestSuite
|
||||
colors: true
|
||||
shuffle: false
|
||||
memory_limit: 1024M
|
||||
log: true
|
||||
backup_globals: true
|
||||
paths:
|
||||
tests: codeception
|
||||
log: codeception/_output
|
||||
data: codeception/_data
|
||||
helpers: codeception/_support
|
||||
envs: ../../../humhub/tests/config/env
|
||||
config:
|
||||
test_entry_url: http://localhost:8080/index-test.php
|
||||
26
tests/codeception/_bootstrap.php
Normal file
26
tests/codeception/_bootstrap.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Initial module test bootstrap. Loads default HumHub test bootstrap.
|
||||
*/
|
||||
|
||||
$testRoot = dirname(__DIR__);
|
||||
|
||||
\Codeception\Configuration::append(['test_root' => $testRoot]);
|
||||
codecept_debug('Module root: ' . $testRoot);
|
||||
|
||||
$humhubPath = getenv('HUMHUB_PATH');
|
||||
if ($humhubPath === false) {
|
||||
$moduleConfig = require $testRoot . '/config/test.php';
|
||||
if (isset($moduleConfig['humhub_root'])) {
|
||||
$humhubPath = $moduleConfig['humhub_root'];
|
||||
} else {
|
||||
$humhubPath = dirname(__DIR__, 5);
|
||||
}
|
||||
}
|
||||
|
||||
\Codeception\Configuration::append(['humhub_root' => $humhubPath]);
|
||||
codecept_debug('HumHub Root: ' . $humhubPath);
|
||||
|
||||
$globalConfig = require $humhubPath . '/protected/humhub/tests/codeception/_loadConfig.php';
|
||||
require $globalConfig['humhub_root'] . '/protected/humhub/tests/codeception/_bootstrap.php';
|
||||
7
tests/codeception/_support/UnitTester.php
Normal file
7
tests/codeception/_support/UnitTester.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace space_profiles;
|
||||
|
||||
class UnitTester extends \humhub\tests\codeception\_support\HumHubDbTestCase
|
||||
{
|
||||
}
|
||||
3
tests/codeception/config/unit.php
Normal file
3
tests/codeception/config/unit.php
Normal file
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
|
||||
return \tests\codeception\_support\HumHubTestConfiguration::getSuiteConfig('unit');
|
||||
9
tests/codeception/unit.suite.yml
Normal file
9
tests/codeception/unit.suite.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
class_name: UnitTester
|
||||
modules:
|
||||
enabled:
|
||||
- tests\codeception\_support\CodeHelper
|
||||
- Yii2
|
||||
config:
|
||||
Yii2:
|
||||
configFile: 'codeception/config/unit.php'
|
||||
transaction: false
|
||||
69
tests/codeception/unit/ProfileAccessTest.php
Normal file
69
tests/codeception/unit/ProfileAccessTest.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace space_profiles;
|
||||
|
||||
use humhub\modules\space\models\Space;
|
||||
use humhub\modules\space_profiles\helpers\ProfileAccess;
|
||||
use humhub\modules\user\models\User;
|
||||
|
||||
class ProfileAccessTest extends UnitTester
|
||||
{
|
||||
public function testPublicSpaceAllowsGuest(): void
|
||||
{
|
||||
$space = $this->createSpaceMock(Space::VISIBILITY_ALL, true, false);
|
||||
|
||||
$this->assertTrue(ProfileAccess::canView($space, null));
|
||||
}
|
||||
|
||||
public function testMembersOnlyBlocksGuestAndAllowsUser(): void
|
||||
{
|
||||
$space = $this->createSpaceMock(Space::VISIBILITY_REGISTERED_ONLY, true, false);
|
||||
$user = $this->createUserMock(false);
|
||||
|
||||
$this->assertFalse(ProfileAccess::canView($space, null));
|
||||
$this->assertTrue(ProfileAccess::canView($space, $user));
|
||||
}
|
||||
|
||||
public function testPrivateSpaceRequiresMembership(): void
|
||||
{
|
||||
$user = $this->createUserMock(false);
|
||||
$nonMemberSpace = $this->createSpaceMock(Space::VISIBILITY_NONE, true, false);
|
||||
$memberSpace = $this->createSpaceMock(Space::VISIBILITY_NONE, true, true);
|
||||
|
||||
$this->assertFalse(ProfileAccess::canView($nonMemberSpace, $user));
|
||||
$this->assertTrue(ProfileAccess::canView($memberSpace, $user));
|
||||
}
|
||||
|
||||
public function testDisabledSpaceDenied(): void
|
||||
{
|
||||
$space = $this->createSpaceMock(Space::VISIBILITY_ALL, false, true);
|
||||
|
||||
$this->assertFalse(ProfileAccess::canView($space, $this->createUserMock(true)));
|
||||
}
|
||||
|
||||
private function createSpaceMock(int $visibility, bool $enabled, bool $isMember): Space
|
||||
{
|
||||
$space = $this->getMockBuilder(Space::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods(['isMember'])
|
||||
->getMock();
|
||||
|
||||
$space->visibility = $visibility;
|
||||
$space->status = $enabled ? Space::STATUS_ENABLED : Space::STATUS_ARCHIVED;
|
||||
$space->method('isMember')->willReturn($isMember);
|
||||
|
||||
return $space;
|
||||
}
|
||||
|
||||
private function createUserMock(bool $isAdmin): User
|
||||
{
|
||||
$user = $this->getMockBuilder(User::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods(['isSystemAdmin'])
|
||||
->getMock();
|
||||
|
||||
$user->method('isSystemAdmin')->willReturn($isAdmin);
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
40
tests/codeception/unit/ProfileHtmlSanitizerTest.php
Normal file
40
tests/codeception/unit/ProfileHtmlSanitizerTest.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace space_profiles;
|
||||
|
||||
use humhub\modules\space_profiles\helpers\ProfileHtmlSanitizer;
|
||||
|
||||
class ProfileHtmlSanitizerTest extends UnitTester
|
||||
{
|
||||
public function testSanitizeRemovesScriptsAndHandlers(): void
|
||||
{
|
||||
$input = '<script>alert(1)</script><div class="hdr" onclick="alert(2)">OK</div>';
|
||||
|
||||
$output = ProfileHtmlSanitizer::sanitize($input);
|
||||
|
||||
$this->assertStringNotContainsString('<script', $output);
|
||||
$this->assertStringNotContainsString('onclick=', $output);
|
||||
$this->assertStringContainsString('<div class="hdr">OK</div>', $output);
|
||||
}
|
||||
|
||||
public function testSanitizeStripsJavascriptUris(): void
|
||||
{
|
||||
$input = '<a href="javascript:alert(1)">bad</a><a href="https://example.org">good</a>';
|
||||
|
||||
$output = ProfileHtmlSanitizer::sanitize($input);
|
||||
|
||||
$this->assertStringNotContainsString('javascript:', $output);
|
||||
$this->assertStringContainsString('https://example.org', $output);
|
||||
}
|
||||
|
||||
public function testSanitizeScopesEmbeddedCss(): void
|
||||
{
|
||||
$input = '<style>body{color:red}.hdr{font-weight:bold}</style><div class="hdr">Title</div>';
|
||||
|
||||
$output = ProfileHtmlSanitizer::sanitize($input);
|
||||
|
||||
$this->assertStringContainsString('.rescue-profile-scope', $output);
|
||||
$this->assertStringContainsString('.rescue-profile-scope .hdr', $output);
|
||||
$this->assertStringNotContainsString('<style>body{color:red}', $output);
|
||||
}
|
||||
}
|
||||
52
tests/codeception/unit/SpaceProfileFormTest.php
Normal file
52
tests/codeception/unit/SpaceProfileFormTest.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace space_profiles;
|
||||
|
||||
use humhub\modules\space\models\Space;
|
||||
use humhub\modules\space_profiles\models\forms\SpaceProfileForm;
|
||||
|
||||
class SpaceProfileFormTest extends UnitTester
|
||||
{
|
||||
public function testValidMinimalDataPassesValidation(): void
|
||||
{
|
||||
$space = Space::findOne(['id' => 2]);
|
||||
$this->assertNotNull($space, 'Expected fixture space with id=2 to exist.');
|
||||
|
||||
$form = new SpaceProfileForm(['contentContainer' => $space]);
|
||||
$form->profile_name = 'Test Rescue Space';
|
||||
$form->city = 'Boston';
|
||||
$form->state = 'MA';
|
||||
$form->zip_code = '02110';
|
||||
$form->contact_email = 'team@example.org';
|
||||
$form->contact_phone = '(617) 555-1212';
|
||||
$form->website_url = 'https://example.org';
|
||||
|
||||
$this->assertTrue($form->validate());
|
||||
}
|
||||
|
||||
public function testStateMustBeTwoCharacters(): void
|
||||
{
|
||||
$space = Space::findOne(['id' => 2]);
|
||||
$this->assertNotNull($space, 'Expected fixture space with id=2 to exist.');
|
||||
|
||||
$form = new SpaceProfileForm(['contentContainer' => $space]);
|
||||
$form->profile_name = 'Invalid State Case';
|
||||
$form->state = 'Massachusetts';
|
||||
|
||||
$this->assertFalse($form->validate(['state']));
|
||||
$this->assertArrayHasKey('state', $form->getErrors());
|
||||
}
|
||||
|
||||
public function testContactEmailMustBeValid(): void
|
||||
{
|
||||
$space = Space::findOne(['id' => 2]);
|
||||
$this->assertNotNull($space, 'Expected fixture space with id=2 to exist.');
|
||||
|
||||
$form = new SpaceProfileForm(['contentContainer' => $space]);
|
||||
$form->profile_name = 'Invalid Email Case';
|
||||
$form->contact_email = 'not-an-email';
|
||||
|
||||
$this->assertFalse($form->validate(['contact_email']));
|
||||
$this->assertArrayHasKey('contact_email', $form->getErrors());
|
||||
}
|
||||
}
|
||||
1
tests/codeception/unit/_bootstrap.php
Normal file
1
tests/codeception/unit/_bootstrap.php
Normal file
@@ -0,0 +1 @@
|
||||
<?php
|
||||
6
tests/config/common.php
Normal file
6
tests/config/common.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Shared config for all suites.
|
||||
*/
|
||||
return [];
|
||||
8
tests/config/test.php
Normal file
8
tests/config/test.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'modules' => ['space_profiles'],
|
||||
'fixtures' => [
|
||||
'default',
|
||||
],
|
||||
];
|
||||
3
tests/config/unit.php
Normal file
3
tests/config/unit.php
Normal file
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
|
||||
return [];
|
||||
45
views/profile/templates/rescue-center.php
Normal file
45
views/profile/templates/rescue-center.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
use yii\helpers\Html;
|
||||
|
||||
/* @var \humhub\modules\space\models\Space $space */
|
||||
/* @var \humhub\modules\space_profiles\models\SpaceProfile $profile */
|
||||
/* @var string $searchBlockWidget */
|
||||
?>
|
||||
|
||||
<div class="media" style="margin-bottom:20px;">
|
||||
<div class="media-left" style="padding-right:15px;">
|
||||
<?php if (!empty($profile->icon_path)): ?>
|
||||
<?= Html::img($profile->icon_path, ['style' => 'width:96px;height:96px;object-fit:cover;border-radius:10px;']) ?>
|
||||
<?php else: ?>
|
||||
<div style="width:96px;height:96px;border-radius:10px;background:#f3f5f7;display:flex;align-items:center;justify-content:center;">
|
||||
<i class="fa fa-paw fa-2x text-muted"></i>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="media-body">
|
||||
<h2 style="margin-top:0;margin-bottom:8px;"><?= Html::encode($space->name) ?></h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($profile->header_html)): ?>
|
||||
<section style="margin-bottom:20px;"><?= $profile->header_html ?></section>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($profile->body_html)): ?>
|
||||
<section style="margin-bottom:20px;"><?= $profile->body_html ?></section>
|
||||
<?php endif; ?>
|
||||
|
||||
<section style="margin-bottom:20px;">
|
||||
<?php if (class_exists($searchBlockWidget)): ?>
|
||||
<?= $searchBlockWidget::widget(['contentContainer' => $space]) ?>
|
||||
<?php else: ?>
|
||||
<div class="well well-sm" style="margin-bottom:0;">
|
||||
<?= Yii::t('SpaceProfilesModule.base', 'Animal Management plugin integration point is ready. The block will appear here when that plugin is installed.') ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<?php if (!empty($profile->footer_html)): ?>
|
||||
<section><?= $profile->footer_html ?></section>
|
||||
<?php endif; ?>
|
||||
93
views/profile/view.php
Normal file
93
views/profile/view.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
use humhub\modules\space\models\Space;
|
||||
use humhub\modules\space_profiles\components\TemplateRegistry;
|
||||
use humhub\modules\space_profiles\models\SpaceProfile;
|
||||
use yii\helpers\Html;
|
||||
|
||||
/* @var Space $space */
|
||||
/* @var SpaceProfile|null $profile */
|
||||
/* @var bool $isPublicRoute */
|
||||
|
||||
$backgroundStyle = '';
|
||||
if ($profile && !empty($profile->background_image_path)) {
|
||||
$backgroundStyle = 'background-image:url(' . Html::encode($profile->background_image_path) . ');background-size:cover;background-position:center;';
|
||||
}
|
||||
|
||||
$searchBlockWidget = '\\humhub\\modules\\animal_management\\widgets\\SearchAnimalProfilesBlock';
|
||||
$templateView = TemplateRegistry::resolveView($profile ? $profile->template_key : null);
|
||||
$spaceDescription = trim((string)$space->about);
|
||||
if ($spaceDescription === '') {
|
||||
$spaceDescription = trim((string)$space->description);
|
||||
}
|
||||
|
||||
if ($spaceDescription === '') {
|
||||
$spaceDescription = trim((string)($profile?->description ?? ''));
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="panel panel-default rescue-profile-scope" style="<?= $backgroundStyle ?>">
|
||||
<div class="panel-body" style="background:rgba(255,255,255,0.93);border-radius:8px;">
|
||||
<?php if (!empty($isPublicRoute) && $space->visibility !== Space::VISIBILITY_ALL): ?>
|
||||
<div class="alert alert-warning">
|
||||
<?= Yii::t('SpaceProfilesModule.base', 'This profile is visible to signed-in users based on the space privacy setting.') ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!$profile): ?>
|
||||
<div class="alert alert-info">
|
||||
<?= Yii::t('SpaceProfilesModule.base', 'This rescue has not published its profile yet.') ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?= $this->render($templateView, [
|
||||
'space' => $space,
|
||||
'profile' => $profile,
|
||||
'searchBlockWidget' => $searchBlockWidget,
|
||||
]) ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php $this->beginBlock('sidebar'); ?>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<?php if (!$profile): ?>
|
||||
<div class="text-muted">
|
||||
<?= Yii::t('SpaceProfilesModule.base', 'Contact details will appear after the profile is published.') ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div style="font-weight:700;font-size:18px;text-align:center;margin-bottom:12px;">
|
||||
<?= Html::encode($space->name) ?>
|
||||
</div>
|
||||
<div><?= Html::encode($profile->address) ?></div>
|
||||
<div style="margin-bottom:10px;"><?= Html::encode($profile->city) ?>, <?= Html::encode($profile->state) ?> <?= Html::encode($profile->zip) ?></div>
|
||||
<div style="margin-bottom:6px;">
|
||||
<strong><?= Yii::t('SpaceProfilesModule.base', 'Email') ?>:</strong>
|
||||
<a href="mailto:<?= Html::encode($profile->email) ?>"><?= Html::encode($profile->email) ?></a>
|
||||
</div>
|
||||
<div>
|
||||
<strong><?= Yii::t('SpaceProfilesModule.base', 'Phone') ?>:</strong>
|
||||
<a href="tel:<?= Html::encode(preg_replace('/[^0-9+]/', '', (string)$profile->phone) ?? '') ?>"><?= Html::encode($profile->phone) ?></a>
|
||||
</div>
|
||||
|
||||
<?php if ($spaceDescription !== '' || trim((string)$profile->mission_statement) !== '' || trim((string)$profile->animals_we_accept) !== ''): ?>
|
||||
<hr style="margin:12px 0;">
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($spaceDescription !== ''): ?>
|
||||
<div style="margin-bottom:10px;"><?= nl2br(Html::encode($spaceDescription)) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (trim((string)$profile->mission_statement) !== ''): ?>
|
||||
<div style="margin-bottom:4px;"><strong><?= Yii::t('SpaceProfilesModule.base', 'Mission Statement') ?>:</strong></div>
|
||||
<div style="margin-bottom:10px;"><?= nl2br(Html::encode($profile->mission_statement)) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (trim((string)$profile->animals_we_accept) !== ''): ?>
|
||||
<div style="margin-bottom:4px;"><strong><?= Yii::t('SpaceProfilesModule.base', 'Animals We Accept') ?>:</strong></div>
|
||||
<div><?= nl2br(Html::encode($profile->animals_we_accept)) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php $this->endBlock(); ?>
|
||||
10
views/public/view.php
Normal file
10
views/public/view.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
/* @var \humhub\modules\space\models\Space $space */
|
||||
/* @var \humhub\modules\space_profiles\models\SpaceProfile|null $profile */
|
||||
|
||||
echo $this->render('@space_profiles/views/profile/view', [
|
||||
'space' => $space,
|
||||
'profile' => $profile,
|
||||
'isPublicRoute' => true,
|
||||
]);
|
||||
95
views/settings/index.php
Normal file
95
views/settings/index.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
use humhub\modules\rescue_foundation\components\UploadStandards;
|
||||
use humhub\modules\space_profiles\models\forms\SpaceProfileForm;
|
||||
use humhub\widgets\Button;
|
||||
use yii\bootstrap\ActiveForm;
|
||||
use yii\helpers\Html;
|
||||
|
||||
/* @var SpaceProfileForm $model */
|
||||
/* @var string|null $subNav */
|
||||
|
||||
$profile = $model->getProfile();
|
||||
?>
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><?= Yii::t('SpaceProfilesModule.base', '<strong>Space Profile</strong> Settings') ?></div>
|
||||
|
||||
<?php if (!empty($subNav)): ?>
|
||||
<?= $subNav ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="panel-body">
|
||||
<div class="help-block">
|
||||
<?= Yii::t('SpaceProfilesModule.base', 'Configure your rescue profile content, optional HTML regions, and branding assets.') ?>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<?= Yii::t('SpaceProfilesModule.base', 'Rescue name and description are inherited from this Space\'s name and about/description settings.') ?>
|
||||
</div>
|
||||
|
||||
<?php $form = ActiveForm::begin(['options' => ['enctype' => 'multipart/form-data']]); ?>
|
||||
|
||||
<?= $form->errorSummary($model, ['class' => 'alert alert-danger']) ?>
|
||||
|
||||
<?= $form->field($model, 'template_key')->dropDownList($model->getTemplateOptions()) ?>
|
||||
<?= $form->field($model, 'address') ?>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4"><?= $form->field($model, 'city') ?></div>
|
||||
<div class="col-md-4"><?= $form->field($model, 'state') ?></div>
|
||||
<div class="col-md-4"><?= $form->field($model, 'zip') ?></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6"><?= $form->field($model, 'email') ?></div>
|
||||
<div class="col-md-6"><?= $form->field($model, 'phone') ?></div>
|
||||
</div>
|
||||
|
||||
<?= $form->field($model, 'animals_we_accept')->textarea(['rows' => 3]) ?>
|
||||
<?= $form->field($model, 'mission_statement')->textarea(['rows' => 4]) ?>
|
||||
|
||||
<hr>
|
||||
|
||||
<?= $form->field($model, 'header_html')->textarea(['rows' => 4]) ?>
|
||||
<?= $form->field($model, 'body_html')->textarea(['rows' => 6]) ?>
|
||||
<?= $form->field($model, 'footer_html')->textarea(['rows' => 4]) ?>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<?= Yii::t('SpaceProfilesModule.base', 'Only HTML/CSS for profile regions is allowed. JavaScript and script-like content are removed automatically.') ?>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="help-block">
|
||||
<?= Yii::t('SpaceProfilesModule.base', 'Allowed image formats: {formats}. Maximum file size: {size} MB.', [
|
||||
'formats' => implode(', ', UploadStandards::imageExtensions()),
|
||||
'size' => number_format(UploadStandards::IMAGE_MAX_BYTES / 1024 / 1024, 1),
|
||||
]) ?>
|
||||
</div>
|
||||
|
||||
<?php if ($profile && !empty($profile->icon_path)): ?>
|
||||
<div class="form-group">
|
||||
<label class="control-label"><?= Yii::t('SpaceProfilesModule.base', 'Current icon') ?></label>
|
||||
<div><?= Html::img($profile->icon_path, ['style' => 'max-width:96px;max-height:96px;border-radius:8px;']) ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?= $form->field($model, 'iconFile')->fileInput(['accept' => '.jpg,.jpeg,.png,.webp']) ?>
|
||||
<?= $form->field($model, 'removeIcon')->checkbox() ?>
|
||||
|
||||
<?php if ($profile && !empty($profile->background_image_path)): ?>
|
||||
<div class="form-group">
|
||||
<label class="control-label"><?= Yii::t('SpaceProfilesModule.base', 'Current background image') ?></label>
|
||||
<div><?= Html::img($profile->background_image_path, ['style' => 'max-width:320px;border-radius:8px;']) ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?= $form->field($model, 'backgroundImageFile')->fileInput(['accept' => '.jpg,.jpeg,.png,.webp']) ?>
|
||||
<?= $form->field($model, 'removeBackgroundImage')->checkbox() ?>
|
||||
|
||||
<?= Button::save()->submit() ?>
|
||||
|
||||
<?php ActiveForm::end(); ?>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user