Files
ave-cms/system/App/Frontend/DocumentRevisionPreview.php
2026-07-27 12:58:44 +03:00

139 lines
4.3 KiB
PHP

<?php
/*
|--------------------------------------------------------------------------------------
| AVE.cms
|--------------------------------------------------------------------------------------
| @package AVE.cms
| @file system/App/Frontend/DocumentRevisionPreview.php
| @author AVE.cms <support@ave-cms.ru>
| @copyright 2007-2026 (c) AVE.cms
| @link https://ave-cms.ru
| @version 3.3
*/
namespace App\Frontend;
defined('BASEPATH') || die('Direct access to this location is not allowed.');
use App\Common\Auth;
use App\Common\Session;
use App\Content\ContentTables;
use DB;
/** Short-lived, session-bound authorization for public revision previews. */
class DocumentRevisionPreview
{
const MAX_TTL = 300;
public static function issue($documentId, $revisionId, $userId, $ttl = self::MAX_TTL)
{
$documentId = (int) $documentId;
$revisionId = (int) $revisionId;
$userId = (int) $userId;
if ($documentId <= 0 || $revisionId <= 0 || $userId <= 0) {
throw new \InvalidArgumentException('Некорректные данные предпросмотра ревизии');
}
$now = time();
$payload = self::encode(json_encode(array(
'v' => 1,
'd' => $documentId,
'r' => $revisionId,
'u' => $userId,
'iat' => $now,
'exp' => $now + max(1, min(self::MAX_TTL, (int) $ttl)),
), JSON_UNESCAPED_SLASHES));
$signature = self::encode(hash_hmac('sha256', $payload, self::key(), true));
return $payload . '.' . $signature;
}
public static function validate($token, $documentId, $revisionId)
{
$token = trim((string) $token);
if ($token === '' || strlen($token) > 1024 || substr_count($token, '.') !== 1) {
return false;
}
list($payload, $signature) = explode('.', $token, 2);
$binarySignature = self::decode($signature);
if ($binarySignature === false || strlen($binarySignature) !== 32) {
return false;
}
$expected = hash_hmac('sha256', $payload, self::key(), true);
if (!hash_equals($expected, $binarySignature)) {
return false;
}
$json = self::decode($payload);
$data = $json === false ? null : json_decode($json, true);
if (!is_array($data)) {
return false;
}
$now = time();
$issuedAt = isset($data['iat']) ? (int) $data['iat'] : 0;
$expiresAt = isset($data['exp']) ? (int) $data['exp'] : 0;
$user = Auth::systemUser();
return isset($data['v']) && (int) $data['v'] === 1
&& isset($data['d']) && (int) $data['d'] === (int) $documentId
&& isset($data['r']) && (int) $data['r'] === (int) $revisionId
&& isset($data['u']) && is_array($user) && (int) $data['u'] === (int) $user['id']
&& Auth::systemUserCan('manage_documents')
&& $issuedAt > 0 && $issuedAt <= $now + 30
&& $expiresAt >= $now && $expiresAt > $issuedAt
&& $expiresAt <= $issuedAt + self::MAX_TTL
&& self::canEditDocument((int) $documentId, $user);
}
protected static function canEditDocument($documentId, array $systemUser)
{
$document = DB::query(
'SELECT rubric_id,document_author_id FROM %b WHERE Id=%i LIMIT 1',
ContentTables::table('documents'),
(int) $documentId
)->getAssoc();
$publicUser = Auth::publicUser();
if (!$document || !is_array($publicUser) || empty($publicUser['group'])) {
return false;
}
$resolver = new RubricPermissionResolver();
$permissions = $resolver->resolve((int) $document['rubric_id'], (int) $publicUser['group']);
if ($resolver->allows($permissions, 'editall')) {
return true;
}
$authorId = (int) $document['document_author_id'];
return in_array('editown', $permissions, true)
&& ($authorId === (int) $systemUser['id'] || $authorId === (int) $publicUser['id']);
}
protected static function key()
{
return hash('sha256', 'avecms:document-revision-preview:' . Session::csrfToken(), true);
}
protected static function encode($value)
{
return rtrim(strtr(base64_encode((string) $value), '+/', '-_'), '=');
}
protected static function decode($value)
{
$value = (string) $value;
if ($value === '' || !preg_match('/^[A-Za-z0-9_-]+$/', $value)) {
return false;
}
$padding = strlen($value) % 4;
if ($padding > 0) {
$value .= str_repeat('=', 4 - $padding);
}
return base64_decode(strtr($value, '-_', '+/'), true);
}
}