This commit is contained in:
2025-08-25 00:33:14 -05:00
parent bbca9c4024
commit cc69c75cca
142 changed files with 16614 additions and 0 deletions

View File

@@ -0,0 +1,78 @@
<?php
/**
* @package AkeebaEngage
* @copyright Copyright (c)2020-2025 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
defined('_JEXEC') or die();
/**
* View Template for comments display
*
* This is the main view template used when comments are being displayed e.g. at the end of an article.
*
* This provides the outer HTML structure of the comments.
*
* It loads the following view templates:
* - default_list.php The threaded list of comments
* - default_login.php Login form for guest users
* - default_form.php Comment / reply submission form
*/
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Language\Text;
/** @var \Akeeba\Component\Engage\Site\View\Comments\HtmlView $this */
$cParams = ComponentHelper::getParams('com_engage');
?>
<section id="akengage-comments-section" class="akengage-outer-container"
aria-label="<?= Text::_('COM_ENGAGE_COMMENTS_SECTION_HEADER') ?>">
<h3 class="akengage-title h4 border-bottom mb-2" data-toc-skip>
<?= Text::plural($this->headerKey, $this->pagination->total, $this->title) ?>
</h3>
<?= $this->loadPosition('engage-before-comments') ?>
<?php if ($this->pagination->total): ?>
<div class="akengage-list-container">
<?= $this->loadTemplate('list') ?>
</div>
<?= $this->loadPosition('engage-after-comments') ?>
<?php if ($this->pagination->pagesTotal > 1): ?>
<div class="akengage-pagination">
<div class="akengage-pagination-pages pagination" itemscope itemtype="http://www.schema.org/SiteNavigationElement">
<?= $this->pagination->getPagesLinks() ?>
</div>
</div>
<?php endif; ?>
<?php endif; ?>
<?php if (!$this->areCommentsClosed && $this->user->guest && !$this->perms['create']): ?>
<?= $this->loadTemplate('login') ?>
<?php endif; ?>
<?php if ($this->perms['create'] && !$this->areCommentsClosed): ?>
<?= $this->loadTemplate('form') ?>
<?php endif; ?>
<?php if ($this->perms['create'] && $this->areCommentsClosed): ?>
<div class="alert alert-info">
<h3 class="alert-heading">
<?= Text::_('COM_ENGAGE_COMMENTS_LBL_CLOSED_HEADER') ?>
</h3>
<p>
<?php if ($this->areCommentsClosedAfterTime): ?>
<?= Text::_('COM_ENGAGE_COMMENTS_LBL_CLOSED_AFTERTIME') ?>
<?php else: ?>
<?= Text::_('COM_ENGAGE_COMMENTS_LBL_CLOSED_BODY') ?>
<?php endif; ?>
</p>
</div>
<?php endif; ?>
</section>

View File

@@ -0,0 +1,90 @@
<?php
/**
* @package AkeebaEngage
* @copyright Copyright (c)2020-2025 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
defined('_JEXEC') or die();
/**
* View Template for the submitting comments.
*
* This is called by default.php
*/
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Uri\Uri;
/** @var \Akeeba\Component\Engage\Site\View\Comments\HtmlView $this */
$cParams = ComponentHelper::getParams('com_engage');
$badUx = ($cParams->get('comments_reply_bad_ux', 0) == 1) && empty($this->form->getValue('body'));
HTMLHelper::_('behavior.formvalidator');
?>
<?php if ($badUx): ?>
<div class="akengage-comment-hider" id="akengage-comment-hider">
<button type="button"
id="akengage-comment-hider-button"
class="btn btn-primary">
<?= Text::_('COM_ENGAGE_COMMENTS_FORM_HEADER'); ?>
</button>
</div>
<?php endif; ?>
<form action="<?= Route::_('index.php?option=com_engage&task=comment.save') ?>"
method="post" name="akengage-comment-form" id="akengageCommentForm"
class="form-validate <?= $badUx ? 'd-none' : ''; ?>"
style="<?= $badUx ? 'display: none;' : ''; ?>"
aria-label="<?= Text::_('COM_ENGAGE_COMMENTS_FORM_HEADER', true) ?>"
>
<input type="hidden" name="returnurl" value="<?= base64_encode(Uri::getInstance()->toString(['scheme', 'user', 'pass', 'host', 'port', 'path', 'query', 'fragment'])) ?>">
<input type="hidden" name="view" value="">
<input type="hidden" name="id" value="">
<?= HTMLHelper::_('form.token') ?>
<div class="mt-3 pt-2 mb-2 border-top border-2 border-dark">
<h3 class="h1 my-3">
<?= Text::_('COM_ENGAGE_COMMENTS_FORM_HEADER') ?>
</h3>
<?= $this->loadPosition('engage-before-reply'); ?>
<div id="akengage-comment-inreplyto-wrapper" class="alert alert-info d-none">
<div class="d-flex flex-wrap">
<div class="flex-grow-1">
<?= Text::_('COM_ENGAGE_COMMENTS_FORM_INREPLYTO_LABEL'); ?>
<span id="akengage-comment-inreplyto-name" class="text-secondary fw-bold">Some User</span>
</div>
<button id="akengage-comment-inreplyto-cancel"
type="button"
class="ms-2 btn btn-sm btn-outline-danger"
><?= Text::_('COM_ENGAGE_COMMENTS_FORM_CANCELREPLY'); ?></button>
</div>
</div>
<?php foreach (array_keys($this->form->getFieldsets()) as $fieldSet)
{
echo $this->form->renderFieldset($fieldSet);
} ?>
<div class="control-group">
<div class="controls">
<button type="submit"
class="btn btn-lg btn-primary w-100 akengage-comment-submit-btn"
>
<span class="fa fa-comment-dots" aria-hidden="true"></span>
<?= Text::_('COM_ENGAGE_COMMENTS_FORM_EDIT_BTN_SUBMIT') ?>
</button>
</div>
</div>
<?= $this->loadPosition('engage-after-reply'); ?>
</div>
</form>

View File

@@ -0,0 +1,286 @@
<?php
/**
* @package AkeebaEngage
* @copyright Copyright (c)2020-2025 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
defined('_JEXEC') or die();
/**
* View Template for the threaded display of comments.
*
* Loaded from default.php
*/
use Akeeba\Component\Engage\Administrator\Helper\Avatar;
use Akeeba\Component\Engage\Administrator\Helper\UserFetcher;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\User\User;
// Maximum avatar width, in pixels.
$maxAvatarWidth = 48;
/** @var \Akeeba\Component\Engage\Site\View\Comments\HtmlView $this */
$previousLevel = 0;
$openListItem = 0;
$parentIds = [0 => 0];
$parentNames = [0 => ''];
foreach ($this->items as $comment):
$user = !empty($comment->created_by) && empty($comment->name) ? UserFetcher::getUser($comment->created_by) : new User();
if (empty($comment->created_by) || !empty($comment->name)) {
$user->name = $comment->name;
$user->email = $comment->email;
}
$parentIds[$comment->depth] = $comment->id;
$parentNames[$comment->depth] = $user->name;
// Deeper level comment. Indent with <ul> tags
if ($comment->depth > $previousLevel):
?>
<?php for ($level = $previousLevel + 1; $level <= $comment->depth; $level++): ?>
<ul class="akengage-comment-list akengage-comment-list--level<?= $level ?> list-unstyled">
<?php endfor; ?>
<?php // Shallower level comment. Outdent with </ul> tags
elseif ($comment->depth < $previousLevel): ?>
<?php if ($openListItem): $openListItem--; ?>
</li>
<?php endif; ?>
<?php for ($level = $previousLevel - 1; $level >= $comment->depth; $level--): ?>
</ul>
<?php if ($openListItem): $openListItem--; ?>
</li>
<?php endif; ?>
<?php endfor; ?>
<?php // Same level comment. Close the <li> tag.
else: ?>
<?php $openListItem--; ?>
</li>
<?php endif; ?>
<?php
$previousLevel = $comment->depth;
$avatar = Avatar::getUserAvatar($comment->created_by, $maxAvatarWidth, $comment->email);
$profile = Avatar::getProfileURL($user);
$commentDate = Factory::getDate($comment->created)->setTimezone($this->userTimezone);
$ipLookupURL = $this->getIPLookupURL($comment->ip);
$isModified = !empty($comment->modified_by) && !empty($comment->modified) && (
empty($comment->created_by) || empty($comment->created) || (
($comment->modified_by != $comment->created_by) &&
($comment->modified != $comment->created)
)
);
if ($isModified)
{
if ($comment->modified_by == $comment->created_by)
{
// If the comment is modified by the created by user, use the public name determined at the top of the file.
$modifiedBy = $user->name;
}
else
{
// Someone else modified the comment. Use their name.
$modifiedUser = UserFetcher::getUser($comment->modified_by);
// If the user is no longer available, use '???'
$modifiedBy = ($modifiedUser === null || $modifiedUser->guest) ? Text::_('COM_ENGAGE_LBL_COMMENT_MODIFIED_NO_LONGER_AVAILABLE') : $modifiedUser->name;
}
}
$openListItem++;
$this->ensureHasParentInfo($comment, $parentIds, $parentNames);
$bsCommentStateClass = ($comment->enabled == 1) ? 'secondary' : (($comment->enabled == -3) ? 'warning' : 'danger')
?>
<li class="akengage-comment-item mb-2">
<article
class="akengage-comment--<?= ($comment->enabled == 1) ? 'primary' : (($comment->enabled == -3) ? 'spam' : 'unpublished') ?> border-start border-4 border-<?= $bsCommentStateClass ?> ps-2 mb-2"
id="akengage-comment-<?= $comment->id ?>" itemscope itemtype="http://schema.org/Comment">
<span itemprop="dateCreated" content="<?= $commentDate->toISO8601(false) ?>"></span>
<span itemprop="datePublished" content="<?= $commentDate->toISO8601(false) ?>"></span>
<footer
itemprop="author" itemscope itemtype="http://schema.org/Person"
class="akengage-comment-properties d-flex flex-row gap-1 mb-1 bg-light p-1 small border-bottom border-2">
<?php if (!empty($avatar)): ?>
<div class="akengage-commenter-avatar-container d-none d-sm-block flex-shrink-1" style="max-width: <?= (int) $maxAvatarWidth ?>px">
<?php if (empty($profile)): ?>
<img src="<?= $avatar ?>" alt="" class="akengage-commenter-avatar img-fluid rounded-3 shadow-sm" itemprop="image">
<?php else: ?>
<a href="<?= $profile ?>" class="akengage-commenter-profile" itemprop="url" rel="noopener">
<img src="<?= $avatar ?>"
alt=""
class="akengage-commenter-avatar img-fluid rounded-3 shadow-sm" itemprop="image">
</a>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="akengange-comment-head d-flex flex-column w-100">
<div class="akengange-commenter-name d-flex flex-row flex-wrap gap-3 align-items-center mb-1">
<span itemprop="name" class="fw-bold flex-grow-1"><?= $this->escape($user->name) ?></span>
<?php if ($this->perms['state']): ?>
<div>
<?php if ($user->authorise('core.manage', $comment->asset_id)): ?>
<span class="akengage-commenter-ismoderator fa fa-star text-warning" aria-hidden="true"></span>
<?php elseif (!$user->guest): ?>
<span class="akengage-commenter-isuser fa fa-user text-secondary" aria-hidden="true"></span>
<?php endif; ?>
<?php if (!$user->guest): ?>
<span class="akengage-commenter-username font-monospace text-success"><?= $this->escape($user->username) ?></span>
<?php elseif ($this->perms['state']): ?>
<span class="akengage-commenter-isguest fa fa-user-friends text-danger" aria-hidden="true"></span>
<span class="akengage-commenter-email font-monospace text-muted"><?= $this->escape($user->email) ?></span>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<div class="akengage-comment-info d-flex flex-row flex-wrap gap-2 align-items-center">
<div class="akengage-comment-permalink flex-grow-1">
<?php
$tempUri = clone Uri::getInstance();
$tempUri->setFragment(sprintf('akengage-comment-%u', $comment->id));
$tempUri->setVar('akengage_cid', $comment->id);
?>
<a href="<?= $tempUri->toString() ?>"
class="text-body text-decoration-none"
>
<?= $commentDate->format(Text::_('DATE_FORMAT_LC2'), true) ?>
</a>
</div>
<div class="akengage-comment-actions d-flex gap-1">
<?php if ($this->perms['state']): ?>
<span class="akengage-comment-publish_unpublish">
<?php if ($comment->enabled == 1): ?>
<button class="akengage-comment-unpublish-btn btn btn-sm btn-outline-secondary"
data-akengageid="<?= $comment->id ?>">
<?= Text::_('COM_ENGAGE_COMMENTS_BTN_UNPUBLISH') ?>
</button>
<?php elseif ($comment->enabled == 0): ?>
<button class="akengage-comment-publish-btn btn btn-sm btn-outline-secondary"
data-akengageid="<?= $comment->id ?>">
<?= Text::_('COM_ENGAGE_COMMENTS_BTN_PUBLISH') ?>
</button>
<?php endif; ?>
</span>
<?php if($comment->enabled == -3): ?>
<span class="akengage-comment-mark-ham">
<button class="akengage-comment-markham-btn btn btn-sm btn-outline-success"
data-akengageid="<?= $comment->id ?>"
title="<?= Text::_('COM_ENGAGE_COMMENTS_BTN_MARKHAM_TITLE') ?>">
<?= Text::_('COM_ENGAGE_COMMENTS_BTN_MARKHAM') ?>
</button>
</span>
<?php if ($this->perms['delete']): ?>
<span class="akengage-comment-mark-spam">
<button class="akengage-comment-markspam-btn btn btn-sm btn-outline-danger"
data-akengageid="<?= $comment->id ?>"
title="<?= Text::_('COM_ENGAGE_COMMENTS_BTN_MARKSPAM_TITLE') ?>">
<?= Text::_('COM_ENGAGE_COMMENTS_BTN_MARKSPAM') ?>
</button>
</span>
<?php endif; ?>
<?php else: ?>
<span class="akengage-comment-mark-possiblespam">
<button class="akengage-comment-possiblespam-btn btn btn-sm btn-outline-warning"
data-akengageid="<?= $comment->id ?>"
title="<?= Text::_('COM_ENGAGE_COMMENTS_BTN_POSSIBLESPAM_TITLE') ?>">
<?= Text::_('COM_ENGAGE_COMMENTS_BTN_POSSIBLESPAM') ?>
</button>
</span>
<?php endif; ?>
<?php endif; ?>
<?php if ($this->perms['delete']): ?>
<span class="akengage-comment-delete">
<button class="akengage-comment-delete-btn btn btn-sm btn-outline-danger"
data-akengageid="<?= $comment->id ?>">
<?= Text::_('COM_ENGAGE_COMMENTS_BTN_DELETE') ?>
</button>
</span>
<?php endif; ?>
<?php if ($this->perms['edit'] || (($this->user->id === $user->id) && $this->perms['own'])): ?>
<span class="akengage-comment-edit">
<button class="akengage-comment-edit-btn btn btn-sm btn-outline-primary"
data-akengageid="<?= $comment->id ?>">
<?= Text::_('COM_ENGAGE_COMMENTS_BTN_EDIT') ?>
</button>
</span>
<?php endif; ?>
</div>
</div>
<?php if ($this->perms['edit'] || $this->user->authorise('core.manage', $comment->asset_id)): ?>
<div>
<?php if (!empty($ipLookupURL)): ?>
<span class="akengage-comment-ip">
<a href="<?= $ipLookupURL ?>" target="_blank">
<?= Text::sprintf('COM_ENGAGE_COMMENTS_IP', $comment->ip ?? '???') ?>
</a>
</span>
<?php else: ?>
<span class="akengage-comment-ip">
<?= Text::sprintf('COM_ENGAGE_COMMENTS_IP', $comment->ip ?? '???') ?>
</span>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</footer>
<?php if ($comment->enabled == -3): ?>
<div class="akengage-comment-publish-type bg-warning text-white fw-bold p-2">
<?= Text::_('COM_ENGAGE_COMMENTS_TYPE_SPAM') ?>
</div>
<?php elseif ($comment->enabled != 1): ?>
<div class="akengage-comment-publish-type bg-danger text-white fw-bold p-2">
<?= Text::_('COM_ENGAGE_COMMENTS_TYPE_UNPUBLISHED') ?>
</div>
<?php endif ?>
<div class="akengage-comment-body" itemprop="text">
<?= HTMLHelper::_('engage.processCommentTextForDisplay', $comment->body) ?>
<?php if ($isModified): ?>
<div class="my-2 border-top border-1 border-muted text-muted small">
<?= Text::sprintf('COM_ENGAGE_LBL_COMMENT_MODIFIED', Factory::getDate($comment->modified)->setTimezone($this->userTimezone)->format(Text::_('DATE_FORMAT_LC2'), true), $modifiedBy) ?>
</div>
<?php endif; ?>
</div>
<?php if ($this->perms['create']): ?>
<div class="akengage-comment-reply">
<?php // You can reply to $this->maxLevel - 1 level comments only. Replies to deeper nested comments are to the $this->maxLevel - 1 level parent. ?>
<button class="akengage-comment-reply-btn btn btn-sm btn-outline-primary mb-1"
data-akengageid="<?= ($comment->depth < $this->maxLevel) ? $comment->id : $parentIds[$this->maxLevel - 1] ?>"
data-akengagereplyto="<?= $this->escape(($comment->depth < $this->maxLevel) ? $user->name : $parentNames[$this->maxLevel - 1]) ?>"
>
<?= Text::_('COM_ENGAGE_COMMENTS_BTN_REPLY') ?>
</button>
</div>
<?php endif; ?>
</article>
<?php endforeach; ?>
<?php if ($openListItem): ?>
<?php $openListItem--; ?>
</li>
<?php endif; ?>
<?php for ($level = $previousLevel; $level >= 1; $level--): ?>
</ul>
<?php if ($openListItem): ?>
<?php $openListItem--; ?>
</li>
<?php endif; ?>
<?php endfor; ?>

View File

@@ -0,0 +1,45 @@
<?php
/**
* @package AkeebaEngage
* @copyright Copyright (c)2020-2025 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
defined('_JEXEC') or die();
/**
* View Template for the guest users login form
*
* Loaded from default.php
*/
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Language\Text;
/** @var \Akeeba\Component\Engage\Site\View\Comments\HtmlView $this */
$cParams = ComponentHelper::getParams('com_engage');
$loginModule = $cParams->get('login_module', '-1');
$moduleContent = (empty($loginModule) || ($loginModule === '-1')) ? '' : trim($this->loadModule($loginModule));
$positionContent = trim($this->loadPosition('engage-login'));
/**
* A reason for this to happen is that site owner wants discussion to be open to invitation-only members of the site but
* visible by anyone. This is mostly relevant in political organizations, NGOs and local / closed community
* organizations where a small number of people are openly discussing a public interest issue, but they don't want to
* allow random people to detract the conversation.
*/
if (empty($moduleContent) && empty($positionContent))
{
return;
}
?>
<footer id="akeeba-engage-login">
<h4>
<?= Text::_('COM_ENGAGE_COMMENTS_LOGIN_HEAD') ?>
</h4>
<?= $moduleContent ?>
<?= $positionContent ?>
</footer>

View File

@@ -0,0 +1,118 @@
<!--
* Copyright (C) 2025 Moko Consulting <jmiller@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<!--FILE INFORMATION
* DEFGROUP: Joomla.Site
* INGROUP: Templates.Moko-Cassiopeia
* FILE: index.html
* BRIEF: Security redirect page to block folder access and forward to site root.
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
/**
* @defgroup Dolibarr
* @file index.html (embedded script)
* @version 1.0.0
* @brief Security redirect logic. Replaces the current history entry with the site root.
* @details This script computes the absolute root URL using `location.origin` and
* forwards the user immediately. It prevents leaving the protected folder
* in the browser history by default.
*
* @section VARIABLES
* @var {Object} opts Configuration options for the redirect behavior.
* @var {string} opts.fallbackPath Path used when `location.origin` cannot be determined.
* @var {number} opts.delayMs Optional delay in milliseconds before redirecting.
* @var {"replace"|"assign"} opts.behavior Navigation method used for the redirect.
*
* @section OPTIONS
* - opts.fallbackPath: default "/" (root path)
* - opts.delayMs: default 0 (immediate)
* - opts.behavior: one of
* * "replace" — calls `location.replace(url)`; does not keep the folder page in history.
* * "assign" — calls `location.assign(url)`; keeps an extra history entry.
*/
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>