Version 4.7.8

This commit is contained in:
Neo committed 2025-12-19 16:10:42 -08:00
1 parent 7b7404e741
commit fd22d99e69
688 files changed
+19814 -10890

No files matched your search

+2 -1
View File
@@ -7,7 +7,7 @@
* primary concern and you are using an opcode cache. PLEASE DO NOT EDIT THIS
* FILE, changes will be overwritten the next time the script is run.
*
* @version 4.14.0
* @version 4.15.0
*
* @warning
* You must *not* include any other HTML Purifier files before this file,
@@ -107,6 +107,7 @@ require 'HTMLPurifier/AttrDef/HTML/Bool.php';
require 'HTMLPurifier/AttrDef/HTML/Nmtokens.php';
require 'HTMLPurifier/AttrDef/HTML/Class.php';
require 'HTMLPurifier/AttrDef/HTML/Color.php';
require 'HTMLPurifier/AttrDef/HTML/ContentEditable.php';
require 'HTMLPurifier/AttrDef/HTML/FrameTarget.php';
require 'HTMLPurifier/AttrDef/HTML/ID.php';
require 'HTMLPurifier/AttrDef/HTML/Pixels.php';
+3 -3
View File
@@ -19,7 +19,7 @@
*/
/*
HTML Purifier 4.14.0 - Standards Compliant HTML Filtering
HTML Purifier 4.15.0 - Standards Compliant HTML Filtering
Copyright (C) 2006-2008 Edward Z. Yang
This library is free software; you can redistribute it and/or
@@ -58,12 +58,12 @@ class HTMLPurifier
* Version of HTML Purifier.
* @type string
*/
public $version = '4.14.0';
public $version = '4.15.0';
/**
* Constant with version of HTML Purifier.
*/
const VERSION = '4.14.0';
const VERSION = '4.15.0';
/**
* Global configuration object.
@@ -101,6 +101,7 @@ require_once $__dir . '/HTMLPurifier/AttrDef/HTML/Bool.php';
require_once $__dir . '/HTMLPurifier/AttrDef/HTML/Nmtokens.php';
require_once $__dir . '/HTMLPurifier/AttrDef/HTML/Class.php';
require_once $__dir . '/HTMLPurifier/AttrDef/HTML/Color.php';
require_once $__dir . '/HTMLPurifier/AttrDef/HTML/ContentEditable.php';
require_once $__dir . '/HTMLPurifier/AttrDef/HTML/FrameTarget.php';
require_once $__dir . '/HTMLPurifier/AttrDef/HTML/ID.php';
require_once $__dir . '/HTMLPurifier/AttrDef/HTML/Pixels.php';
@@ -0,0 +1,16 @@
<?php
class HTMLPurifier_AttrDef_HTML_ContentEditable extends HTMLPurifier_AttrDef
{
public function validate($string, $config, $context)
{
$allowed = array('false');
if ($config->get('HTML.Trusted')) {
$allowed = array('', 'true', 'false');
}
$enum = new HTMLPurifier_AttrDef_Enum($allowed);
return $enum->validate($string, $config, $context);
}
}
@@ -8,6 +8,11 @@
class HTMLPurifier_AttrTransform_NameSync extends HTMLPurifier_AttrTransform
{
/**
* @type HTMLPurifier_AttrDef_HTML_ID
*/
public $idDef;
public function __construct()
{
$this->idDef = new HTMLPurifier_AttrDef_HTML_ID();
@@ -24,6 +24,11 @@ class HTMLPurifier_AttrTransform_SafeParam extends HTMLPurifier_AttrTransform
*/
private $uri;
/**
* @type HTMLPurifier_AttrDef_Enum
*/
public $wmode;
public function __construct()
{
$this->uri = new HTMLPurifier_AttrDef_URI(true); // embedded
@@ -41,6 +41,7 @@ class HTMLPurifier_AttrTypes
$this->info['IAlign'] = self::makeEnum('top,middle,bottom,left,right');
$this->info['LAlign'] = self::makeEnum('top,bottom,left,right');
$this->info['FrameTarget'] = new HTMLPurifier_AttrDef_HTML_FrameTarget();
$this->info['ContentEditable'] = new HTMLPurifier_AttrDef_HTML_ContentEditable();
// unimplemented aliases
$this->info['ContentType'] = new HTMLPurifier_AttrDef_Text();
@@ -22,6 +22,8 @@ class HTMLPurifier_ChildDef_List extends HTMLPurifier_ChildDef
// XXX: This whole business with 'wrap' is all a bit unsatisfactory
public $elements = array('li' => true, 'ul' => true, 'ol' => true);
public $whitespace;
/**
* @param array $children
* @param HTMLPurifier_Config $config
+1 -1
View File
@@ -21,7 +21,7 @@ class HTMLPurifier_Config
* HTML Purifier's version
* @type string
*/
public $version = '4.14.0';
public $version = '4.15.0';
/**
* Whether or not to automatically finalize
+1 -1
View File
@@ -176,7 +176,7 @@ class HTMLPurifier_ElementDef
if (!empty($def->content_model)) {
$this->content_model =
str_replace("#SUPER", $this->content_model, $def->content_model);
str_replace("#SUPER", (string)$this->content_model, $def->content_model);
$this->child = false;
}
if (!empty($def->content_model_type)) {
+4 -4
View File
@@ -398,8 +398,8 @@ class HTMLPurifier_Encoder
// characters to their true byte-wise ASCII/UTF-8 equivalents.
$str = strtr($str, self::testEncodingSupportsASCII($encoding));
return $str;
} elseif ($encoding === 'iso-8859-1') {
$str = utf8_encode($str);
} elseif ($encoding === 'iso-8859-1' && function_exists('mb_convert_encoding')) {
$str = mb_convert_encoding($str, 'UTF-8', 'ISO-8859-1');
return $str;
}
$bug = HTMLPurifier_Encoder::testIconvTruncateBug();
@@ -450,8 +450,8 @@ class HTMLPurifier_Encoder
// Normal stuff
$str = self::iconv('utf-8', $encoding . '//IGNORE', $str);
return $str;
} elseif ($encoding === 'iso-8859-1') {
$str = utf8_decode($str);
} elseif ($encoding === 'iso-8859-1' && function_exists('mb_convert_encoding')) {
$str = mb_convert_encoding($str, 'ISO-8859-1', 'UTF-8');
return $str;
}
trigger_error('Encoding not supported', E_USER_ERROR);
@@ -17,6 +17,7 @@ class HTMLPurifier_HTMLModule_CommonAttributes extends HTMLPurifier_HTMLModule
'class' => 'Class',
'id' => 'ID',
'title' => 'CDATA',
'contenteditable' => 'ContentEditable',
),
'Lang' => array(),
'I18N' => array(
@@ -31,6 +31,16 @@ class HTMLPurifier_Injector_RemoveSpansWithoutAttributes extends HTMLPurifier_In
*/
private $context;
/**
* @type SplObjectStorage
*/
private $markForDeletion;
public function __construct()
{
$this->markForDeletion = new SplObjectStorage();
}
public function prepare($config, $context)
{
$this->attrValidator = new HTMLPurifier_AttrValidator();
@@ -64,7 +74,7 @@ class HTMLPurifier_Injector_RemoveSpansWithoutAttributes extends HTMLPurifier_In
if ($current instanceof HTMLPurifier_Token_End && $current->name === 'span') {
// Mark closing span tag for deletion
$current->markForDeletion = true;
$this->markForDeletion->attach($current);
// Delete open span tag
$token = false;
}
@@ -75,7 +85,8 @@ class HTMLPurifier_Injector_RemoveSpansWithoutAttributes extends HTMLPurifier_In
*/
public function handleEnd(&$token)
{
if ($token->markForDeletion) {
if ($this->markForDeletion->contains($token)) {
$this->markForDeletion->detach($token);
$token = false;
}
}
+1 -1
View File
@@ -78,7 +78,7 @@ class HTMLPurifier_Length
if ($this->n === '0' && $this->unit === false) {
return true;
}
if (!ctype_lower($this->unit)) {
if ($this->unit === false || !ctype_lower($this->unit)) {
$this->unit = strtolower($this->unit);
}
if (!isset(HTMLPurifier_Length::$allowedUnits[$this->unit])) {
+7 -2
View File
@@ -48,6 +48,11 @@ class HTMLPurifier_Lexer
*/
public $tracksLineNumbers = false;
/**
* @type HTMLPurifier_EntityParser
*/
private $_entity_parser;
// -- STATIC ----------------------------------------------------------
/**
@@ -306,8 +311,8 @@ class HTMLPurifier_Lexer
{
// normalize newlines to \n
if ($config->get('Core.NormalizeNewlines')) {
$html = str_replace("\r\n", "\n", $html);
$html = str_replace("\r", "\n", $html);
$html = str_replace("\r\n", "\n", (string)$html);
$html = str_replace("\r", "\n", (string)$html);
}
if ($config->get('HTML.Trusted')) {
+1 -1
View File
@@ -4410,7 +4410,7 @@ class HTML5TreeConstructer
foreach ($token['attr'] as $attr) {
if (!$el->hasAttribute($attr['name'])) {
$el->setAttribute($attr['name'], $attr['value']);
$el->setAttribute($attr['name'], (string)$attr['value']);
}
}
@@ -29,6 +29,7 @@ class HTMLPurifier_PropertyListIterator extends FilterIterator
/**
* @return bool
*/
#[\ReturnTypeWillChange]
public function accept()
{
$key = $this->getInnerIterator()->key();
@@ -20,6 +20,7 @@ class HTMLPurifier_StringHash extends ArrayObject
* @param mixed $index
* @return mixed
*/
#[\ReturnTypeWillChange]
public function offsetGet($index)
{
$this->accessed[$index] = true;
@@ -35,7 +35,7 @@ class HTMLPurifier_URIFilter_HostBlacklist extends HTMLPurifier_URIFilter
public function filter(&$uri, $config, $context)
{
foreach ($this->blacklist as $blacklisted_host_fragment) {
if (strpos($uri->host, $blacklisted_host_fragment) !== false) {
if ($uri->host !== null && strpos($uri->host, $blacklisted_host_fragment) !== false) {
return false;
}
}
@@ -100,11 +100,11 @@ class HTMLPurifier_URIFilter_Munge extends HTMLPurifier_URIFilter
$string = $uri->toString();
// always available
$this->replace['%s'] = $string;
$this->replace['%r'] = $context->get('EmbeddedURI', true);
$token = $context->get('CurrentToken', true);
$this->replace['%n'] = $token ? $token->name : null;
$this->replace['%m'] = $context->get('CurrentAttr', true);
$this->replace['%p'] = $context->get('CurrentCSSProperty', true);
$this->replace['%r'] = $context->get('EmbeddedURI', true) ?: '';
$token = $context->get('CurrentToken', true) ?: '';
$this->replace['%n'] = $token ? $token->name : '';
$this->replace['%m'] = $context->get('CurrentAttr', true) ?: '';
$this->replace['%p'] = $context->get('CurrentCSSProperty', true) ?: '';
// not always available
if ($this->secretKey) {
$this->replace['%t'] = hash_hmac("sha256", $string, $this->secretKey);
+1 -1
View File
@@ -32,7 +32,7 @@ abstract class _AppQueries
*
* @return array
*/
public static function queries($app)
public static function queries($app): array
{
return [
'moduleAccess' => new \IPS\Api\GraphQL\Queries\ModuleAccess($app)
@@ -30,7 +30,7 @@ abstract class _CoverPhotoField
*
* @return array
*/
public static function getDefinition($name)
public static function getDefinition($name): array
{
return [
'type' => new ObjectType([
+1 -1
View File
@@ -30,7 +30,7 @@ abstract class _TagsEditField
*
* @return array
*/
public static function getDefinition($name)
public static function getDefinition($name): array
{
return [
'type' => new ObjectType([
+4 -2
View File
@@ -33,10 +33,12 @@ class _GraphQL
*
* @param string $query The query to execute
* @param array $variables Variables to include in query
* @param \IPS\Member|NULL $member Member to check or NULL for currently logged in member.
* @return array GraphQL response
*/
public static function execute( string $query, array $variables = [], array $member = NULL )
public static function execute( string $query, array $variables = [], ?\IPS\Member $member = NULL )
{
$member = $member ?: \IPS\Member::loggedIn();
/* Register our GraphQL library */
\IPS\IPS::$PSR0Namespaces['GraphQL'] = \IPS\ROOT_PATH . "/system/3rd_party/graphql-php";
@@ -49,7 +51,7 @@ class _GraphQL
$query,
NULL, // $rootValue
[
'member' => \IPS\Member::loggedIn()
'member' => $member
], // $context
$variables
);
+3 -2
View File
@@ -38,8 +38,9 @@ class _ModuleAccess
/*
* Query arguments
*/
public function args()
public function args(): array
{
return [];
}
/**
@@ -56,7 +57,7 @@ class _ModuleAccess
* @param mixed Value passed into this resolver
* @param array Arguments
* @param array Context values
* @return \IPS\core\Stream
* @return \IPS\Application
*/
public function resolve($val, $args, $context)
{
+26 -26
View File
@@ -47,25 +47,25 @@ class _TypeRegistry
}
/**
* @return QueryType
* @return \IPS\Api\GraphQL\Types\QueryType
*/
public static function query()
public static function query(): \IPS\Api\GraphQL\Types\QueryType
{
return self::$query ?: (self::$query = new \IPS\Api\GraphQL\Types\QueryType());
}
/**
* @return MutationType
* @return \IPS\Api\GraphQL\Types\MutationType
*/
public static function mutation()
public static function mutation(): \IPS\Api\GraphQL\Types\MutationType
{
return self::$mutation ?: (self::$mutation = new \IPS\Api\GraphQL\Types\MutationType());
}
/**
* @return ItemStateType
* @return \IPS\Api\GraphQL\Types\ItemStateType
*/
public static function itemState()
public static function itemState(): \IPS\Api\GraphQL\Types\ItemStateType
{
return self::$itemState ?: (self::$itemState = new \IPS\Api\GraphQL\Types\ItemStateType());
}
@@ -73,47 +73,47 @@ class _TypeRegistry
/**
* @return ImageType
*/
public static function image()
public static function image(): \IPS\Api\GraphQL\Types\ImageType
{
return self::$image ?: (self::$image = new \IPS\Api\GraphQL\Types\ImageType());
}
/**
* @return ReputationType
* @return \IPS\Api\GraphQL\Types\ReputationType
*/
public static function reputation()
public static function reputation(): \IPS\Api\GraphQL\Types\ReputationType
{
return self::$reputation ?: (self::$reputation = new \IPS\Api\GraphQL\Types\ReputationType());
}
/**
* @return RichTextType
* @return \IPS\Api\GraphQL\Types\RichTextType
*/
public static function richText()
public static function richText(): \IPS\Api\GraphQL\Types\RichTextType
{
return self::$richText ?: (self::$richText = new \IPS\Api\GraphQL\Types\RichTextType());
}
/**
* @return URLType
* @return \IPS\Api\GraphQL\Types\UrlType
*/
public static function url()
public static function url(): \IPS\Api\GraphQL\Types\UrlType
{
return self::$url ?: (self::$url = new \IPS\Api\GraphQL\Types\UrlType());
}
/**
* @return FollowType
* @return \IPS\Api\GraphQL\Types\FollowType
*/
public static function follow()
public static function follow(): \IPS\Api\GraphQL\Types\FollowType
{
return self::$follow ?: (self::$follow = new \IPS\Api\GraphQL\Types\FollowType());
}
/**
* @return ModuleAccessType
* @return \IPS\Api\GraphQL\Types\ModuleAccessType
*/
public static function moduleAccess()
public static function moduleAccess(): \IPS\Api\GraphQL\Types\ModuleAccessType
{
return self::$moduleAccess ?: (self::$moduleAccess = new \IPS\Api\GraphQL\Types\ModuleAccessType());
}
@@ -121,7 +121,7 @@ class _TypeRegistry
/**
* @return \GraphQL\Type\Definition\IDType
*/
public static function id()
public static function id(): \GraphQL\Type\Definition\IDType
{
return Type::id();
}
@@ -129,7 +129,7 @@ class _TypeRegistry
/**
* @return \GraphQL\Type\Definition\StringType
*/
public static function string()
public static function string(): \GraphQL\Type\Definition\StringType
{
return Type::string();
}
@@ -137,7 +137,7 @@ class _TypeRegistry
/**
* @return \GraphQL\Type\Definition\IntType
*/
public static function int()
public static function int(): \GraphQL\Type\Definition\IntType
{
return Type::int();
}
@@ -145,7 +145,7 @@ class _TypeRegistry
/**
* @return \GraphQL\Type\Definition\FloatType
*/
public static function float()
public static function float(): \GraphQL\Type\Definition\FloatType
{
return Type::float();
}
@@ -153,7 +153,7 @@ class _TypeRegistry
/**
* @return \GraphQL\Type\Definition\BooleanType
*/
public static function boolean()
public static function boolean(): \GraphQL\Type\Definition\BooleanType
{
return Type::boolean();
}
@@ -161,7 +161,7 @@ class _TypeRegistry
/**
* @return \GraphQL\Type\Definition\ListOfType
*/
public static function listOf($type)
public static function listOf($type): \GraphQL\Type\Definition\ListOfType
{
return new ListOfType($type);
}
@@ -169,12 +169,12 @@ class _TypeRegistry
/**
* @return \GraphQL\Type\Definition\EnumType
*/
public static function eNum($config)
public static function eNum($config): \GraphQL\Type\Definition\EnumType
{
return new EnumType($config);
}
public static function inputObjectType($config)
public static function inputObjectType($config): \GraphQL\Type\Definition\InputObjectType
{
return new InputObjectType($config);
}
@@ -183,7 +183,7 @@ class _TypeRegistry
* @param Type $type
* @return NonNull
*/
public static function nonNull($type)
public static function nonNull($type): \GraphQL\Type\Definition\NonNull
{
return new NonNull($type);
}
+1 -4
View File
@@ -84,13 +84,10 @@ class _MutationType extends ObjectType
}
return $mutationResolvers;
$mutationClass = new $className;
return $mutationClass->resolve($val, $args, $context, $info);
}
catch ( \Exception $e )
{
throw new \IPS\Api\Exception( 'NO_RESOLVE', '3S291/2', 405 );
throw new \IPS\Api\Exception( 'NO_RESOLVE', '3S291/2_graphl', 405 );
}
}
];
+36 -2
View File
@@ -131,10 +131,24 @@ class _OAuthClient extends \IPS\Node\Model
'toggles' => array(
'invision' => array( 'oauth_grant_types_invision', 'oauth_invision_endpoint' ),
'wordpress' => array( 'oauth_wordpress_endpoint' ),
'confidential' => array( 'oauth_grant_types_confidential', 'oauth_redirect_uris', 'oauth_choose_scopes', 'oauth_header_oauth_access_tokens', 'oauth_access_token_length', 'oauth_tab_oauth_scopes' ),
'public' => array( 'oauth_grant_types_public', 'oauth_redirect_uris', 'oauth_choose_scopes', 'oauth_header_oauth_access_tokens', 'oauth_access_token_length', 'oauth_tab_oauth_scopes' ),
'confidential' => array( 'oauth_grant_types_confidential', 'oauth_redirect_uris', 'oauth_choose_scopes', 'oauth_header_oauth_access_tokens', 'oauth_access_token_length', 'oauth_tab_oauth_scopes','oauth_api_access', ),
'public' => array( 'oauth_grant_types_public', 'oauth_redirect_uris', 'oauth_choose_scopes', 'oauth_header_oauth_access_tokens', 'oauth_access_token_length', 'oauth_tab_oauth_scopes', 'oauth_api_access' ),
)
) ) );
$form->add( new \IPS\Helpers\Form\Radio( 'oauth_api_access', $this->api_access ? $this->api_access : 'rest', TRUE, array(
'options' => array(
'rest' => 'oauth_api_type_rest',
'graphql' => 'oauth_api_type_graphql',
'both' => 'oauth_api_type_both',
),
'toggles' => array(
'rest' => array( 'oauth_tab_oauth_scopes', 'oauth_choose_scopes' ),
'both' => array( 'oauth_tab_oauth_scopes', 'oauth_choose_scopes' ),
'graphql' => array( '' ),
)
), NULL, NULL, NULL, 'oauth_api_access' ) );
$form->add( new \IPS\Helpers\Form\Radio( 'oauth_invision_grant_type', $this->client_id ? $this->grant_types : 'authorization_code', NULL, array(
'options' => array(
'authorization_code' => 'invision_grant_type_server_authorization_code',
@@ -304,6 +318,7 @@ class _OAuthClient extends \IPS\Node\Model
$values['oauth_access_token_length'] = 168;
$values['oauth_use_refresh_tokens'] = TRUE;
$values['oauth_refresh_token_length'] = 28;
$values['oauth_api_access'] = 'rest';
$values['scopes'] = array(
array(
'name' => array( 'key' => 'profile', 'desc' => \IPS\Member::loggedIn()->language()->get('oauth_default_scope_profile') ),
@@ -330,6 +345,7 @@ class _OAuthClient extends \IPS\Node\Model
$values['oauth_access_token_length'] = 168;
$values['oauth_use_refresh_tokens'] = TRUE;
$values['oauth_refresh_token_length'] = 28;
$values['oauth_api_access'] = 'rest';
$values['scopes'] = array(
array(
'name' => array( 'key' => 'email', 'desc' => \IPS\Member::loggedIn()->language()->get('oauth_default_scope_email') ),
@@ -342,6 +358,11 @@ class _OAuthClient extends \IPS\Node\Model
$values['oauth_type'] = 'wordpress';
$values['oauth_pkce'] = 'none';
}
elseif ( $values['oauth_api_access'] === 'graphql' )
{
$values['oauth_choose_scopes'] = FALSE;
$values['scopes'] = array();
}
else
{
$values['oauth_type'] = NULL;
@@ -412,6 +433,19 @@ class _OAuthClient extends \IPS\Node\Model
/* Return */
return $values;
}
/**
* Return the custom badge for each row
*
* @return NULL|array Null for no badge, or an array of badge data (0 => CSS class type, 1 => language string, 2 => optional raw HTML to show instead of language string)
*/
public function get__badge()
{
return array(
0 => 'ipsBadge ipsBadge_neutral ipsPos_right ipsMargin_right:half',
1 => 'api_access_' . $this->api_access,
);
}
/**
* [Node] Get Description
+8 -4
View File
@@ -134,9 +134,9 @@ class _Application extends \IPS\Node\Model
/**
* Get Applications
*
* @return array
* @return array<\IPS\Application>
*/
public static function applications()
public static function applications(): array
{
if( static::$gotAll === FALSE )
{
@@ -182,9 +182,9 @@ class _Application extends \IPS\Node\Model
/**
* Get enabled applications
*
* @return array
* @return array<\IPS\Application>
*/
public static function enabledApplications()
public static function enabledApplications(): array
{
$applications = static::applications();
$enabled = array();
@@ -4870,6 +4870,10 @@ EOF;
/* Delete Anonymous Data */
\IPS\Db::i()->delete( 'core_anonymous_posts', \IPS\Db::i()->in( 'anonymous_object_class', $classes ) );
/* Delete Polls */
\IPS\Db::i()->delete( 'core_voters', array( 'poll in (?)', \IPS\Db::i()->select( 'pid', 'core_polls', \IPS\Db::i()->in( 'poll_item_class', $classes ) ) ) );
\IPS\Db::i()->delete( 'core_polls', \IPS\Db::i()->in( 'poll_item_class', $classes ) );
}
/* Delete attachment maps - if the attachment is unused, the regular cleanup task will remove the file later */
@@ -10,6 +10,7 @@
namespace IPS\Content\Api\GraphQL;
use \IPS\Api\GraphQL\TypeRegistry;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
@@ -22,6 +23,18 @@ if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
*/
abstract class _ItemMutator extends ContentMutator
{
public function args(): array
{
return [
'category' => TypeRegistry::nonNull( TypeRegistry::id() ),
'title' => TypeRegistry::nonNull( TypeRegistry::string() ),
'content' => TypeRegistry::nonNull( TypeRegistry::string() ),
'tags' => TypeRegistry::listOf( TypeRegistry::string() ),
'state' => TypeRegistry::itemState(),
'postKey' => TypeRegistry::string()
];
}
/**
* Mark as read
*
+8
View File
@@ -40,6 +40,13 @@ class _ItemController extends \IPS\Api\Controller
{
$where[] = array( \IPS\Db::i()->in( $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['container'], array_map( 'intval', array_filter( explode( ',', \IPS\Request::i()->$containerParam ) ) ) ) );
}
/* IDs */
if ( isset( \IPS\Request::i()->ids ) )
{
$idField = $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnId;
$where[] = array(\IPS\Db::i()->in( $idField, array_map( 'intval', explode(',', \IPS\Request::i()->ids ) ) ) );
}
/* Authors */
if ( isset( \IPS\Request::i()->authors ) )
@@ -488,6 +495,7 @@ class _ItemController extends \IPS\Api\Controller
if( !$item->getPoll() )
{
$poll->starter_id = $item->author()->member_id;
$poll->poll_item_class = \get_class( $item );
}
$formatted = array(
+7 -6
View File
@@ -140,7 +140,7 @@ trait ClubContainer
}
$this->save();
\IPS\Db::i()->update( 'core_clubs_node_map', array( 'name' => $values['club_node_name'], 'public' => $values['club_node_public'] ?? 0), array( 'club_id=? AND node_class=? AND node_id=?', $club->id, $nodeClass, $this->_id ) );
\IPS\Db::i()->update( 'core_clubs_node_map', array( 'name' => $values['club_node_name'], 'public' => $values['club_node_public'] ? $values['club_node_public'] : 0), array( 'club_id=? AND node_class=? AND node_id=?', $club->id, $nodeClass, $this->_id ) );
}
\IPS\Lang::saveCustom( $itemClass::$application, static::$titleLangPrefix . $this->_id, $values['club_node_name'] );
@@ -220,9 +220,10 @@ trait ClubContainer
case $club::TYPE_PUBLIC:
$insert[ 'perm_' . $v ] = 'ca';
break;
case $club::TYPE_OPEN:
case $club::TYPE_CLOSED:
$insert[ 'perm_' . $v ] = ( $this->isPublic() == 2 ) ? "*" : "cm,c{$club->id}";
break;
case $club::TYPE_OPEN:
case $club::TYPE_PRIVATE:
case $club::TYPE_READONLY:
$insert[ 'perm_' . $v ] = "cm,c{$club->id}";
@@ -410,17 +411,17 @@ trait ClubContainer
/**
* Is public
*
* @return Bool
* @return int 0 = not public, 1 = anyone can view, 2 = anyone can participate
*/
public function isPublic()
{
try
{
return (bool) \IPS\Db::i()->select( 'public', 'core_clubs_node_map', array( 'club_id=? AND node_class=? AND node_id=?', $this->club()->id, \get_called_class(), $this->_id ) )->first();
return (int) \IPS\Db::i()->select( 'public', 'core_clubs_node_map', array( 'club_id=? AND node_class=? AND node_id=?', $this->club()->id, \get_called_class(), $this->_id ) )->first();
}
catch ( \UnderflowException $e )
{
return FALSE;
return 0;
}
}
}
+6 -4
View File
@@ -326,7 +326,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
if( $this->isAnonymous() )
{
$guest = new \IPS\Member;
$guest->name = \IPS\Member::loggedIn()->language()->addToStack( "post_anonymously_placename" );
$guest->name = \IPS\Member::loggedIn()->language()->get( "post_anonymously_placename" );
return $guest;
}
elseif ( $this->mapped('author') or !isset( static::$databaseColumnMap['author_name'] ) or !$this->mapped('author_name') )
@@ -3158,10 +3158,11 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
/**
* Get edit history
*
* @param bool $staff Set true for moderators who have permission to view the full log which will show edits not made by the author and private edits
* @param bool $staff Set true for moderators who have permission to view the full log which will show edits not made by the author and private edits
* @param int|NULL $limit
* @return \IPS\Db\Select
*/
public function editHistory( $staff=FALSE )
public function editHistory( $staff=FALSE, $limit=NULL )
{
$idColumn = static::$databaseColumnId;
$where = array( array( 'class=? AND comment_id=?', \get_called_class(), $this->$idColumn ) );
@@ -3169,7 +3170,8 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
{
$where[] = array( '`member`=? AND public=1', $this->author()->member_id );
}
return \IPS\Db::i()->select( '*', 'core_edit_history', $where, 'time DESC' );
return \IPS\Db::i()->select( '*', 'core_edit_history', $where, 'time DESC', $limit );
}
/**
+77 -2
View File
@@ -382,6 +382,14 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
return;
}
/* If we're filtering the topic (live topic questions) then the max commentID in the source will not be the max commentID possible
as not all comments will be shown */
if ( isset( \IPS\Request::i()->ltqid ) )
{
\IPS\Output::i()->json( array( 'error' => 'auto_polling_disabled' ) );
return;
}
/* If guest page caching is on, and we're a guest, disable polling */
if ( !\IPS\Member::loggedIn()->member_id and \IPS\CACHE_PAGE_TIMEOUT )
{
@@ -1271,10 +1279,40 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
\IPS\Output::i()->error( 'node_error', '2S136/1Q', 404, '' );
}
$idColumn = $class::$databaseColumnId;
$where = array( array( 'class=? AND comment_id=?', $class, $item->$idColumn ) );
if ( !\IPS\Member::loggedIn()->modPermission( 'can_view_editlog' ) )
{
$where[] = array( '`member`=? AND public=1', $item->author()->member_id );
}
$table = new \IPS\Helpers\Table\Db( 'core_edit_history', $item->url()->setQueryString( array( 'do' => 'editlog' ) ), $where );
$table->sortBy = 'time';
$table->sortDirection = 'desc';
$table->limit = 10;
$table->tableTemplate = array( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' ), 'commentEditHistoryTable' );
$table->rowsTemplate = array( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' ), 'commentEditHistoryRows' );
$table->parsers = array(
'new' => function( $val )
{
return $val;
},
'old' => function( $val )
{
return $val;
}
);
$table->extra = $item;
$pageParam = $table->getPaginationKey();
if( \IPS\Request::i()->isAjax() AND isset( \IPS\Request::i()->$pageParam ) )
{
\IPS\Output::i()->sendOutput( (string) $table );
}
\IPS\Output::i()->breadcrumb[] = array( NULL, \IPS\Member::loggedIn()->language()->addToStack( 'edit_history_title' ) );
\IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack( 'edit_history_title' );
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->commentEditHistory( $item->editHistory( \IPS\Member::loggedIn()->modPermission( 'can_view_editlog' ) ), $item );
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->commentEditHistory( (string) $table, $item );
}
/**
@@ -2963,6 +3001,37 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
{
throw new \DomainException;
}
$idColumn = $commentClass::$databaseColumnId;
$where = array( array( 'class=? AND comment_id=?', $commentClass, $comment->$idColumn ) );
if ( !\IPS\Member::loggedIn()->modPermission( 'can_view_editlog' ) )
{
$where[] = array( '`member`=? AND public=1', $comment->author()->member_id );
}
$table = new \IPS\Helpers\Table\Db( 'core_edit_history', $item->url()->setQueryString( array( 'do' => 'editlogComment', 'comment' => $comment->$idColumn ) ), $where );
$table->sortBy = 'time';
$table->sortDirection = 'desc';
$table->limit = 10;
$table->tableTemplate = array( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' ), 'commentEditHistoryTable' );
$table->rowsTemplate = array( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' ), 'commentEditHistoryRows' );
$table->parsers = array(
'new' => function( $val )
{
return $val;
},
'old' => function( $val )
{
return $val;
}
);
$table->extra = $comment;
$pageParam = $table->getPaginationKey();
if( \IPS\Request::i()->isAjax() AND isset( \IPS\Request::i()->$pageParam ) )
{
\IPS\Output::i()->sendOutput( (string) $table );
}
/* Display */
$container = NULL;
@@ -2979,7 +3048,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
\IPS\Output::i()->breadcrumb[] = array( $comment->url(), $item->mapped( 'title' ) );
\IPS\Output::i()->breadcrumb[] = array( NULL, \IPS\Member::loggedIn()->language()->addToStack( 'edit_history_title' ) );
\IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack( 'edit_history_title' );
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->commentEditHistory( $comment->editHistory( \IPS\Member::loggedIn()->modPermission( 'can_view_editlog' ) ), $comment );
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->commentEditHistory( (string) $table, $comment );
}
/**
@@ -3529,6 +3598,12 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
$oldItem->rebuildPopularTime();
}
/* Option to do some post split stuff */
if ( method_exists( $item, 'splitComplete' ) )
{
$item->splitComplete( $oldItem, $item, $comments );
}
/* Now reindex all of the comments - we do this last to prevent the "is last comment" flag continously bouncing around while the comments are still being moved */
foreach( $comments as $comment )
{
+95 -26
View File
@@ -320,7 +320,7 @@ abstract class _Item extends \IPS\Content
case 'hidden_reviews':
$val = 0;
break;
case 'is_future_entry':
$val = ( $time->getTimestamp() > time() ) ? 1 : 0;
break;
@@ -425,7 +425,7 @@ abstract class _Item extends \IPS\Content
{
$commentClass = static::$commentClass;
$comment = $commentClass::create( $obj, $values[ static::$formLangPrefix . 'content' ], TRUE, ( !$member->real_name ) ? NULL : $member->real_name, $obj->hidden() ? FALSE : NULL, $member );
$comment = $commentClass::create( $obj, $values[ static::$formLangPrefix . 'content' ], TRUE, ( !$member->real_name ) ? NULL : $member->real_name, $obj->hidden() ? FALSE : NULL, $member, $time );
$idColumn = static::$databaseColumnId;
$commentIdColumn = $commentClass::$databaseColumnId;
@@ -560,6 +560,7 @@ abstract class _Item extends \IPS\Content
{
$val = $val ? $val->pid : NULL;
}
foreach ( \is_array( static::$databaseColumnMap[ $k ] ) ? static::$databaseColumnMap[ $k ] : array( static::$databaseColumnMap[ $k ] ) as $column )
{
$this->$column = $val;
@@ -579,6 +580,12 @@ abstract class _Item extends \IPS\Content
$this->setTags( $values[ static::$formLangPrefix . 'tags' ] ?: array() );
}
/* Future Publishing */
if( static::canFuturePublish( NULL, $this->containerWrapper() ) and isset( static::$databaseColumnMap['future_date'] ) and isset( $values[ static::$formLangPrefix . 'date'] ) )
{
$this->setFuturePublishingDates( $values );
}
/* Post before registering */
if ( isset( $values['guest_email'] ) and ( !$this->containerWrapper() or !$this->containerWrapper()->can( 'add', \IPS\Member::loggedIn(), FALSE ) ) )
{
@@ -591,6 +598,29 @@ abstract class _Item extends \IPS\Content
\IPS\Request::i()->setCookie( 'post_before_register', $this->_logPostBeforeRegistering( $values['guest_email'], isset( \IPS\Request::i()->cookie['post_before_register'] ) ? \IPS\Request::i()->cookie['post_before_register'] : NULL ) );
}
}
/**
* Set future publishing dates
*
* @param array $values Values from form
* @return void
*/
protected function setFuturePublishingDates( array $values )
{
$time = $values[ static::$formLangPrefix . 'date' ] instanceof \IPS\DateTime ? $values[ static::$formLangPrefix . 'date' ] : new \IPS\DateTime;
if( isset( static::$databaseColumnMap['date'] ) )
{
$column = static::$databaseColumnMap['date'];
$this->$column = ( $time->getTimestamp() > time() ) ? $time->getTimestamp() : time();
}
if( isset( static::$databaseColumnMap['future_date'] ) )
{
$column = static::$databaseColumnMap['future_date'];
$this->$column = $time->getTimestamp();
}
}
/**
* Can a given member create this type of content?
@@ -688,7 +718,7 @@ abstract class _Item extends \IPS\Content
/* Title */
if ( isset( static::$databaseColumnMap['title'] ) )
{
$return['title'] = new \IPS\Helpers\Form\Text( static::$formLangPrefix . 'title', $item ? $item->mapped('title') : ( isset( \IPS\Request::i()->title ) ? \IPS\Request::i()->title : NULL ), TRUE, array( 'maxLength' => \IPS\Settings::i()->max_title_length ?: 255, 'bypassProfanity' => \IPS\Helpers\Form\Text::BYPASS_PROFANITY_SWAP ) );
$return['title'] = new \IPS\Helpers\Form\Text( static::$formLangPrefix . 'title', isset( \IPS\Request::i()->title ) ? \IPS\Request::i()->title : $item?->mapped( 'title' ), TRUE, array( 'maxLength' => \IPS\Settings::i()->max_title_length ?: 255, 'bypassProfanity' => \IPS\Helpers\Form\Text::BYPASS_PROFANITY_SWAP ) );
$return['title']->rowClasses[] = 'ipsFieldRow_primary';
$return['title']->rowClasses[] = 'ipsFieldRow_fullWidth';
}
@@ -856,20 +886,69 @@ abstract class _Item extends \IPS\Content
/* Create form element */
if ( $canCreatePoll )
{
$return['poll'] = new \IPS\Helpers\Form\Poll( static::$formLangPrefix . 'poll', $existingPoll, FALSE, array( 'allowPollOnly' => TRUE ) );
$return['poll'] = new \IPS\Helpers\Form\Poll( static::$formLangPrefix . 'poll', $existingPoll, FALSE, array( 'allowPollOnly' => TRUE, 'itemClass' => \get_called_class() ) );
}
}
/* Future date */
if ( \in_array( 'IPS\Content\FuturePublishing', class_implements( \get_called_class() ) ) and static::canFuturePublish( NULL, $container ) and isset( static::$databaseColumnMap['future_date'] ) )
/* Show the future date field for new items or while editing an item, but only if the item wasn't published yet */
if ( static::supportsPublishDate( $item ) and static::canFuturePublish( NULL, $container ) )
{
$column = static::$databaseColumnMap['future_date'];
$return['date'] = new \IPS\Helpers\Form\Date( static::$formLangPrefix . 'date', ( $item and $item->$column ) ? \IPS\DateTime::ts( $item->$column ) : 0, FALSE, array( 'time' => TRUE, 'unlimited' => 0, 'unlimitedLang' => 'immediately'), NULL, NULL, NULL, static::$formLangPrefix . 'date' );
$return['date'] = static::getPublishDateField( $item );
}
return $return;
}
/**
* Returns the Date Field for the publish date
*
* @param \IPS\Content|NULL $item
* @return \IPS\Helpers\Form\Date
*/
protected static function getPublishDateField( \IPS\Content $item = NULL ): \IPS\Helpers\Form\Date
{
/* If it's not published, we don't want to allow any past times */
$column = static::$databaseColumnMap['future_date'];
$minFutureTime = static::getMinimumPublishDate();
$unlimited = 0;
$unlimitedLang = 'immediately';
if( $item )
{
$unlimited = NULL;
$unlimitedLang = NULL;
}
return new \IPS\Helpers\Form\Date( static::$formLangPrefix . 'date', ( $item and $item->$column ) ? \IPS\DateTime::ts( $item->$column ) : 0, FALSE, array( 'time' => TRUE, 'unlimited' => $unlimited, 'unlimitedLang' => $unlimitedLang, 'min' => $minFutureTime ), NULL, NULL, NULL, static::$formLangPrefix . 'date' );
}
/**
* Can the publish date be changed while editing the item?
*
* @var bool
*/
public static bool $allowPublishDateWhileEditing = FALSE;
/**
* Whether this content supports future publish dates
*
* @return bool
*/
protected static function supportsPublishDate( ?\IPS\Content\Item $item ): bool
{
return \in_array( 'IPS\Content\FuturePublishing', class_implements( \get_called_class() ) ) and isset( static::$databaseColumnMap['future_date'] ) AND ( ( $item AND ( $item->isFutureDate() OR static::$allowPublishDateWhileEditing ) OR !$item ) );
}
/**
* Returns the earliest publish date for the new content item , should be the current timestamp for most content types
*
* @return \IPS\DateTime|null
*/
protected static function getMinimumPublishDate(): ?\IPS\DateTime
{
return \IPS\DateTime::create();
}
/**
* Generate the tags form element
*
@@ -1150,6 +1229,13 @@ abstract class _Item extends \IPS\Content
$column = $commentClass::$databaseColumnMap['content'];
$idField = $commentClass::$databaseColumnId;
/* Update the comment date, in case the topic scheduled publish date has changed */
if( isset( $values[ static::$formLangPrefix . 'date' ] ) and $values[ static::$formLangPrefix . 'date' ] instanceof \IPS\DateTime and isset( $commentClass::$databaseColumnMap['date'] ) )
{
$dateColumn = $commentClass::$databaseColumnMap['date'];
$commentObj->$dateColumn = $values[ static::$formLangPrefix . 'date' ]->getTimestamp();
}
if ( $commentObj instanceof \IPS\Content\EditHistory and \IPS\Settings::i()->edit_log )
{
$editIsPublic = \IPS\Member::loggedIn()->group['g_append_edit'] ? $values['comment_log_edit'] : TRUE;
@@ -1244,16 +1330,6 @@ abstract class _Item extends \IPS\Content
$this->publish();
}
}
elseif ( $container AND ! $this->$column )
{
if ( $values[ static::$formLangPrefix . 'date' ] instanceof \IPS\DateTime AND $values[ static::$formLangPrefix . 'date' ]->getTimestamp() > time() )
{
/* Was not future, but now is */
$dateColumn = static::$databaseColumnMap['future_date'];
$this->$dateColumn = $values[ static::$formLangPrefix . 'date' ]->getTimestamp();
$this->unPublish();
}
}
}
/* Post anonymously */
@@ -8503,7 +8579,7 @@ abstract class _Item extends \IPS\Content
$this->$future = 1;
}
$this->save();
$this->onUnpublish( $member );
@@ -8541,12 +8617,6 @@ abstract class _Item extends \IPS\Content
$this->$future = 0;
}
if ( isset( static::$databaseColumnMap['date'] ) )
{
$dateField = \is_array( static::$databaseColumnMap['date'] ) ? static::$databaseColumnMap['date'][0] : static::$databaseColumnMap['date'];
$this->$dateField = time();
}
$this->save();
$this->onPublish( $member );
@@ -8649,7 +8719,6 @@ abstract class _Item extends \IPS\Content
*/
public function onUnpublish( $member )
{
$container = NULL;
if ( method_exists( $this, 'container' ) )
{
try
+12 -8
View File
@@ -1265,16 +1265,20 @@ class _Query extends \IPS\Content\Search\Query
/* This forces MySQL to strongly hint a better index to the optimiser for MySQL 5.7+, even though sorting will not be affected */
if ( !\IPS\CIC )
{
if ( $this->filterByUnread !== FALSE and ( ! mb_stristr( \IPS\Db::i()->server_info, '-MariaDB' ) and \IPS\Db::i()->server_version >= 50700 ) )
try
{
$order .= ", index_date_updated";
}
/* If we're filtering by unread, and not using MariaDB, then force an index because MySQL tends to use the incorrect one in this scenario. */
if( !mb_stristr( \IPS\Db::i()->server_info, '-MariaDB' ) AND $this->filterByUnread === TRUE )
{
$this->forceIndex = 'index_date_updated';
if ( $this->filterByUnread !== FALSE and ( !mb_stristr( \IPS\Db::i()->server_info, '-MariaDB' ) and \IPS\Db::i()->server_version >= 50700 ) )
{
$order .= ", index_date_updated";
}
/* If we're filtering by unread, and not using MariaDB, then force an index because MySQL tends to use the incorrect one in this scenario. */
if ( !mb_stristr( \IPS\Db::i()->server_info, '-MariaDB' ) and $this->filterByUnread === TRUE )
{
$this->forceIndex = 'index_date_updated';
}
}
catch( \Throwable $e ) { /* Do nothing if server_info access fails */ }
}
/* But we're sorting by relevancy, we need to actually select that value with our fancy algorithm */
+1 -1
View File
@@ -51,7 +51,7 @@ class _Pinterest extends \IPS\Content\ShareServices
{
if ( $this->item )
{
return \IPS\Theme::i()->getTemplate( 'sharelinks', 'core' )->pinterest( \IPS\Http\Url::external( 'http://pinterest.com/pin/create/button/' )->setQueryString( 'url', (string) $this->url )->setQueryString( 'media', (string) $this->item->shareImage() ) );
return \IPS\Theme::i()->getTemplate( 'sharelinks', 'core' )->pinterest( \IPS\Http\Url::external( 'https://pinterest.com/pin/create/button/' )->setQueryString( 'url', (string) $this->url )->setQueryString( 'media', (string) $this->item->shareImage() ) );
}
else
{
+6
View File
@@ -222,6 +222,12 @@ trait Solvable
{
return FALSE;
}
/* If we have no replies, it's not solvable yet */
if( $this->commentCount() <= 0 OR ( static::$firstCommentRequired AND $this->commentCount() == 1 ) )
{
return false;
}
if ( $this->containerAllowsSolvable() )
{
+9 -18
View File
@@ -35,7 +35,6 @@ class _DateTime extends \DateTime
$obj->setTimestamp( (int) $timestamp );
if ( !$bypassTimezone AND \IPS\Dispatcher::hasInstance() and \IPS\Member::loggedIn()->timezone )
{
$validTimezone = TRUE;
if( \in_array( \IPS\Member::loggedIn()->timezone, static::getTimezoneIdentifiers() ) )
{
try
@@ -44,24 +43,9 @@ class _DateTime extends \DateTime
}
catch ( \Exception $e )
{
$validTimezone = FALSE;
}
}
else
{
$validTimezone = FALSE;
}
if( ! $validTimezone )
{
\IPS\Member::loggedIn()->timezone = null;
if ( \IPS\Member::loggedIn()->member_id )
{
\IPS\Member::loggedIn()->save();
}
}
}
return $obj;
}
@@ -99,7 +83,14 @@ class _DateTime extends \DateTime
{
return 'America/Argentina/' . \substr( $timezone, 8 );
}
return $timezone;
/* Chromium reports the wrong timezones for these locations - https://bugs.chromium.org/p/chromium/issues/detail?id=580195 */
return match ( $timezone )
{
'Asia/Calcutta' => 'Asia/Kolkata',
'Asia/Katmandu' => 'Asia/Kathmandu',
default => $timezone,
};
}
/**
+3
View File
@@ -51,6 +51,9 @@ class _Developer
if( $updated )
{
\IPS\Plugin\Hook::writeDataFile();
/* Update JS cache bust */
\IPS\Settings::i()->changeValues( array( 'javascript_updated' => time() ) );
}
}
+28 -13
View File
@@ -44,13 +44,6 @@ class _GraphQL extends Api
/* Check our IP address isn't banned */
$this->_checkIpAddressIsAllowed();
/* If this is a pre-flight request, acknowldge it */
// @todo review this approach
if( $_SERVER['REQUEST_METHOD'] === 'OPTIONS' )
{
$this->_respond( '', 200 );
}
/* Authenticate */
$client = NULL;
$this->_setRawCredentials();
@@ -60,23 +53,34 @@ class _GraphQL extends Api
{
$this->_setAccessToken();
$client = \IPS\Api\OAuthClient::load( $this->accessToken['client_id'] );
if( $client->api_access == 'rest' )
{
throw new \IPS\Api\Exception( 'NO_GRAPHQL_ACCESS', '3S426_graphql/1', 403 );
}
\IPS\Member::$loggedInMember = \IPS\Member::load( $this->accessToken['member_id'] );
}
else
{
$client = \IPS\Api\OAuthClient::load( $this->rawApiKey );
/* Check that the API key has access to the GraphQL API */
if( $client->api_access == 'rest' )
{
throw new \IPS\Api\Exception( 'NO_GRAPHQL_ACCESS', '3S426_graphql/1', 403 );
}
\IPS\Member::$loggedInMember = new \IPS\Member;
}
}
catch ( \OutOfRangeException $e )
{
throw new \IPS\Api\Exception( 'INVALID_API_KEY', '3S290/7', 401 );
throw new \IPS\Api\Exception( 'INVALID_API_KEY', '3S290_graphql/7', 401 );
}
/* Check that the OAuth client has access to the GraphQL API */
if ( !$client->graphql )
if( $client->api_access === 'rest' )
{
throw new \IPS\Api\Exception( 'NO_GRAPHQL_ACCESS', '2S291/3', 403 );
throw new \IPS\Api\Exception( 'NO_GRAPHQL_ACCESS', '2S291_graphql/3', 403 );
}
}
catch ( \IPS\Api\Exception $e )
@@ -85,7 +89,7 @@ class _GraphQL extends Api
$response = json_encode( array( 'errors' => array( array( 'message' => $e->getMessage(), 'id' => $e->exceptionCode ) ) ), JSON_PRETTY_PRINT );
/* Do we need to log this? */
if ( \in_array( $e->exceptionCode, array( '2S290/8', '2S290/B', '3S290/7', '3S290/9' ) ) )
if ( \in_array( $e->exceptionCode, array( '2S290_graphql/8', '2S290_graphql/B', '3S290_graphql/7', '3S290_graphql/9' ) ) )
{
$this->_log( $response, $e->getCode(), \in_array( $e->exceptionCode, array( '3S290/7', '3S290/9', '3S290/B' ) ) );
}
@@ -106,11 +110,22 @@ class _GraphQL extends Api
{
/* Work out the query (can either use a JSON-encoded or form-encoded) body */
$query = NULL;
$variables = NULL;
$variables = [];
if( \IPS\Request::i()->query )
{
$query = \IPS\Request::i()->query;
$variables = ( isset( \IPS\Request::i()->variables ) AND \is_array( \IPS\Request::i()->variables ) ) ? \IPS\Request::i()->variables : [];
if( isset( \IPS\Request::i()->variables ) )
{
if( \is_array( \IPS\Request::i()->variables ) )
{
$variables = \IPS\Request::i()->variables;
}
else
{
$variables = json_decode( \IPS\Request::i()->variables, TRUE );
}
}
}
elseif ( $json = json_decode( file_get_contents('php://input'), TRUE ) )
{
+22 -11
View File
@@ -99,7 +99,7 @@ class _Amazon extends \IPS\File
static::$updatedEndpoint = NULL;
}
if ( ! $values['toggle'] )
if ( empty( $values['toggle'] ) )
{
$values['custom_url'] = NULL;
}
@@ -554,10 +554,12 @@ class _Amazon extends \IPS\File
$r = static::makeRequest( $path, 'POST', $this->configuration, $this->configurationId, NULL, NULL, FALSE, FALSE, array( 'uploads' => '' ) );
$uploadId = (string) $r->decodeXml()->UploadId;
return array(
'path' => $path,
'uploadId' => $uploadId
'path' => $path,
'uploadId' => $uploadId,
'final' => $path,
'original' => $this->originalFilename
);
}
@@ -584,15 +586,16 @@ class _Amazon extends \IPS\File
return $ref;
}
/**
* Finalize a chunked upload
*
* @param mixed $ref The reference for this upload as returned by chunkInit()
* @return string The file's reference which can be passed to \IPS\File::get() as the URL
* @throws \RuntimeException
* @param array $ref The reference for this upload as returned by chunkInit()
* @param string $storageConfiguration Storage configuration name
* @return \IPS\File The file object just created
* @throws \RuntimeException
*/
public function chunkFinish( $ref )
public function chunkFinish( array $ref, string $storageConfiguration ): \IPS\File
{
$xml = \IPS\Xml\SimpleXML::create( 'CompleteMultipartUpload', 'http://s3.amazonaws.com/doc/2006-03-01/' );
foreach ( $ref['etags'] as $partNumber => $eTag )
@@ -601,10 +604,18 @@ class _Amazon extends \IPS\File
$part->addChild( 'ETag', $eTag );
$part->addChild( 'PartNumber', $partNumber );
}
static::makeRequest( $ref['path'], 'POST', $this->configuration, $this->configurationId, $xml->asXML(), NULL, FALSE, NULL, array( 'uploadId' => $ref['uploadId'] ) );
return $ref['path'];
$fileObj = \IPS\File::get( $storageConfiguration, $ref['final'] );
/* This isn't preserved for chunk uploads, so we need to set it so apps can get the real value */
if ( !empty( $ref['original'] ) )
{
$fileObj->originalFilename = $ref['original'];
}
return $fileObj;
}
/**
+5 -1
View File
@@ -950,7 +950,11 @@ abstract class _File
/* Make sure name doesn't have anything that may break URLs */
if ( preg_match( '#[^a-zA-Z0-9!\-_\.\*\(\)\@]#', $filename ) )
{
$filename = mt_rand() . '_' . preg_replace( '#[^a-zA-Z0-9!\-_\.\*\(\)\@]#', '', $filename );
$filename = preg_replace( '#[^a-zA-Z0-9!\-_\.\*\(\)\@]#', '', $filename );
if( !$obscure OR \strlen( pathinfo( $filename, PATHINFO_FILENAME ) ) === 0 )
{
$filename = mt_rand() . '_' . $filename;
}
}
if ( $obscure )
+17 -7
View File
@@ -469,7 +469,7 @@ class _FileSystem extends \IPS\File
$this->setFilename( $filename, $obscure );
$return = array( 'temp' => "{$this->configuration['dir']}/{$tempFolder}/chunk-" . md5( uniqid() ) . '.txt', 'final' => "{$folder}/" . $this->filename );
$return = array( 'temp' => "{$this->configuration['dir']}/{$tempFolder}/chunk-" . md5( uniqid() ) . '.txt', 'final' => "{$folder}/" . $this->filename, 'original' => $this->originalFilename );
touch( $return['temp'] );
return $return;
}
@@ -509,18 +509,28 @@ class _FileSystem extends \IPS\File
return $ref;
}
/**
* Finalize a chunked upload
*
* @param mixed $ref The reference for this upload as returned by chunkInit()
* @return string The file's reference which can be passed to \IPS\File::get() as the URL
* @throws \RuntimeException
* @param array $ref The reference for this upload as returned by chunkInit()
* @param string $storageConfiguration Storage configuration name
* @return \IPS\File The file object just created
* @throws \RuntimeException
*/
public function chunkFinish( $ref )
public function chunkFinish( array $ref, string $storageConfiguration ): \IPS\File
{
rename( $ref['temp'], "{$this->configuration['dir']}/{$ref['final']}" );
return $ref['final'];
$fileObj = \IPS\File::get( $storageConfiguration,$ref['final'] );
/* This isn't preserved for chunk uploads, so we need to set it so apps can get the real value */
if( !empty( $ref['original'] ) )
{
$fileObj->originalFilename = $ref['original'];
}
return $fileObj;
}
/* !File System Utility Methods */
+1 -1
View File
@@ -94,7 +94,7 @@ class _Iterator extends \IteratorIterator implements \Countable
}
}
$fileSize = ( $this->fileSizeField AND !empty( $data[ $this->fileSizeField ] ) ) ? $data[ $this->fileSizeField ] : ( $this->fileSizeField === FALSE ? FALSE : NULL );
$fileSize = ( $this->fileSizeField AND isset( $data[ $this->fileSizeField ] ) ) ? $data[ $this->fileSizeField ] : ( $this->fileSizeField === FALSE ? FALSE : NULL );
$obj = \IPS\File::get( $this->storageExtension, $urlField ? $data[ $urlField ] : $data, $fileSize );
if ( $this->replaceNameField and ! empty( $data[ $this->replaceNameField ] ) )
+2 -1
View File
@@ -39,6 +39,7 @@ class _CheckboxSet extends Select
{
$options['multiple'] = TRUE;
$options['showAllNone'] = $options['showAllNone'] ?? TRUE;
$options['condense'] = $options['condense'] ?? TRUE;
parent::__construct( $name, $defaultValue, $required, $options, $customValidationCode, $prefix, $suffix, $id );
if ( !isset( $this->options['descriptions'] ) )
@@ -93,7 +94,7 @@ class _CheckboxSet extends Select
$value = array( $value );
}
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->checkboxset( $this->name, $value, $this->required, $this->parseOptions(), $this->options['multiple'], $this->options['class'], $this->options['disabled'], $this->options['toggles'], NULL, $this->options['unlimited'], $this->options['unlimitedLang'], $this->options['unlimitedToggles'], $this->options['unlimitedToggleOn'], $descriptions, $this->options['impliedUnlimited'], $this->options['showAllNone'] );
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->checkboxset( $this->name, $value, $this->required, $this->parseOptions(), $this->options['multiple'], $this->options['class'], $this->options['disabled'], $this->options['toggles'], NULL, $this->options['unlimited'], $this->options['unlimitedLang'], $this->options['unlimitedToggles'], $this->options['unlimitedToggleOn'], $descriptions, $this->options['impliedUnlimited'], $this->options['showAllNone'], $this->options['condense'] );
}
/**
+22 -2
View File
@@ -290,15 +290,35 @@ class _Date extends FormAbstract
}
/* Check minimum */
try
{
$timezone = $this->options['timezone'] ?: ( \IPS\Member::loggedIn()->timezone ? new \DateTimeZone( \IPS\Member::loggedIn()->timezone ) : NULL );
}
catch ( \Exception $e )
{
$timezone = NULL;
}
if ( $this->value and $this->options['min'] !== NULL and $this->options['min'] > $this->value )
{
throw new \LengthException( \IPS\Member::loggedIn()->language()->addToStack('form_date_min', FALSE, array( 'sprintf' => array( $this->options['min']->localeDate() ) ) ) );
$string = $this->options['min']->setTimeZone( $timezone )->localeDate( \IPS\Member::loggedIn() );
if( $this->options['time'] )
{
$string .=' ' . $this->options['min']->setTimeZone( $timezone )->localeTime( \IPS\Member::loggedIn() );
}
throw new \LengthException( \IPS\Member::loggedIn()->language()->addToStack('form_date_min', FALSE, array( 'sprintf' => array( $string ) ) ) );
}
/* Check maximum */
if ( $this->value and $this->options['max'] !== NULL and $this->options['max'] < $this->value )
{
throw new \LengthException( \IPS\Member::loggedIn()->language()->addToStack('form_date_max', FALSE, array( 'sprintf' => array( $this->options['max']->localeDate() ) ) ) );
$string = $this->options['max']->setTimeZone( $timezone )->localeDate( \IPS\Member::loggedIn() );
if( $this->options['time'] )
{
$string .=' ' . $this->options['max']->setTimeZone( $timezone )->localeTime( \IPS\Member::loggedIn() );
}
throw new \LengthException( \IPS\Member::loggedIn()->language()->addToStack('form_date_max', FALSE, array( 'sprintf' => array( $string ) ) ) );
}
return TRUE;
+5 -2
View File
@@ -95,8 +95,11 @@ class _DateRange extends FormAbstract
*/
public function __construct( $name, $defaultValue=NULL, $required=FALSE, $options=array(), $customValidationCode=NULL, $prefix=NULL, $suffix=NULL, $id=NULL )
{
$this->start = new \IPS\Helpers\Form\Date( "{$name}[start]", isset( $defaultValue['start'] ) ? $defaultValue['start'] : NULL, FALSE, isset( $options['start'] ) ? $options['start'] : array() );
$this->end = new \IPS\Helpers\Form\Date( "{$name}[end]", isset( $defaultValue['end'] ) ? $defaultValue['end'] : NULL, FALSE, isset( $options['end'] ) ? $options['end'] : array() );
$startOptions = isset( $options['start'] ) ? array_merge( $this->defaultOptions['start'], $options['start'] ) : $this->defaultOptions['start'];
$this->start = new \IPS\Helpers\Form\Date( "{$name}[start]", isset( $defaultValue['start'] ) ? $defaultValue['start'] : NULL, FALSE, $startOptions );
$endOptions = isset( $options['end'] ) ? array_merge( $this->defaultOptions['end'], $options['end'] ) : $this->defaultOptions['end'];
$this->end = new \IPS\Helpers\Form\Date( "{$name}[end]", isset( $defaultValue['end'] ) ? $defaultValue['end'] : NULL, FALSE, $endOptions );
$this->unlimitedName = "{$name}_unlimited";
parent::__construct( $name, $defaultValue, $required, $options, $customValidationCode, $prefix, $suffix, $id );
+7 -1
View File
@@ -27,6 +27,7 @@ class _Poll extends FormAbstract
* @code
$defaultOptions = array(
'allowPollOnly' => TRUE, // Controls if "Allow poll only" option is allowed (if the admin has enabled it)
'itemClass' => NULL, // The content item class attached to this poll
);
* @endcode
*/
@@ -103,8 +104,13 @@ EOF;
{
$poll = new \IPS\Poll;
$poll->starter_id = \IPS\Member::loggedIn()->member_id;
if( isset( $this->options['itemClass'] ) )
{
$poll->poll_item_class = $this->options['itemClass'];
}
}
/* Set values */
$data = parent::getValue();
if ( !isset( $data['questions'] ) )
+19 -29
View File
@@ -232,9 +232,7 @@ class _Upload extends FormAbstract
/* If this is the LAST chunk, finish the process */
if ( \IPS\Request::i()->chunk == \IPS\Request::i()->chunks - 1 )
{
$url = $storageClass->chunkFinish( $ref );
$fileObj = \IPS\File::get( $this->options['storageExtension'], $url );
$fileObj = $storageClass->chunkFinish( $ref, $this->options['storageExtension'] );
$fileArray = array(
'error' => NULL,
@@ -247,24 +245,21 @@ class _Upload extends FormAbstract
/* If there is an error, an exception will be thrown and will be caught below like normal */
\IPS\File::validateUpload( $fileArray, $this->options['allowedFileTypes'], $this->options['maxFileSize'] );
if ( $fileObj->isImage() AND ( $this->options['checkImage'] OR \is_array( $this->options['image'] ) ) )
if ( $fileObj->isImage() )
{
try
{
$image = \IPS\Image::create( $fileObj->contents() );
$image = \IPS\Image::create( $fileObj->contents(), $this->options['checkImage'] ?: TRUE );
if( $image::exifSupported() )
{
$image->setExifData( $fileObj->contents() );
}
$hasBeenResized = FALSE;
if( \is_array( $this->options['image'] ) )
{
$hasBeenResized = $image->resizeToMax( $this->options['image']['maxWidth'] ?: NULL, $this->options['image']['maxHeight'] ?: NULL );
}
if ( $hasBeenResized or $image->hasBeenRotated )
$hasBeenResized = $image->resizeToMax( $this->options['image']['maxWidth'] ?? NULL, $this->options['image']['maxHeight'] ?? NULL );
/* If type JPG, image handler may strip meta data on image output */
if( $hasBeenResized OR $image->hasBeenRotated OR $image->type == 'jpeg' )
{
$fileObj->replace( (string) $image );
}
@@ -487,7 +482,7 @@ class _Upload extends FormAbstract
{
/* Get the files we had already */
$return = array();
$tempFiles = iterator_to_array( \IPS\Db::i()->select( 'id,contents,filename,exif,requires_moderation', 'core_files_temp', array( 'upload_key=?', md5( $this->name . session_id() ) ) )->setKeyField('id') );
$tempFiles = iterator_to_array( \IPS\Db::i()->select( 'id,contents,filename,exif,requires_moderation,labels', 'core_files_temp', array( 'upload_key=?', md5( $this->name . session_id() ) ) )->setKeyField('id') );
if ( $this->options['storageExtension'] )
{
$existingName = $this->name . '_existing';
@@ -507,6 +502,7 @@ class _Upload extends FormAbstract
$file->originalFilename = $tempFiles[ $tempId ]['filename'];
$file->exifData = $tempFiles[ $tempId ]['exif'] ? json_decode( $tempFiles[ $tempId ]['exif'], TRUE ) : NULL;
$file->requiresModeration = (bool) $tempFiles[ $tempId ]['requires_moderation'];
$file->labels = $tempFiles[ $tempId ]['labels'];
$return[ $id ] = $file;
}
else
@@ -735,25 +731,19 @@ class _Upload extends FormAbstract
{
/* Resize */
$image = NULL;
$hasBeenResized = FALSE;
if ( \is_array( $options['image'] ) OR $options['checkImage'] )
try
{
try
$image = \IPS\Image::create( $contents, $options['checkImage'] ?: TRUE );
$hasBeenResized = $image->resizeToMax( $options['image']['maxWidth'] ?? NULL, $options['image']['maxHeight'] ?? NULL );
/* If type JPG, image handler may strip meta data on image output */
if( $hasBeenResized OR $image->hasBeenRotated OR $image->type == 'jpeg' )
{
$image = \IPS\Image::create( $contents );
if( \is_array( $options['image'] ) )
{
$hasBeenResized = $image->resizeToMax( $options['image']['maxWidth'] ?: NULL, $options['image']['maxHeight'] ?: NULL );
}
if ( $hasBeenResized or $image->hasBeenRotated )
{
$contents = (string) $image;
}
$contents = (string) $image;
}
catch ( \Exception $e ) {}
}
catch ( \Exception $e ) {}
}
return $contents;
+24
View File
@@ -253,6 +253,10 @@ class _Content extends Table
}
}
/* Make sure we initialize the default sort fields so that we can properly get the column sort direction */
$this->defaultSortBy = $this->sortBy;
$this->defaultSortDirection = $this->sortDirection;
/* Do any multi-mod */
if ( isset( \IPS\Request::i()->modaction ) )
{
@@ -1061,4 +1065,24 @@ class _Content extends Table
{
return $this->container;
}
/**
* Return the sort direction to use for links
*
* @note Abstracted so other table helper instances can adjust as needed
* @param string $column Sort by string
* @return string [asc|desc]
*/
public function getSortDirection( $column )
{
$class = $this->class;
/* ID and Title columns should be ascending */
if( $column != $this->defaultSortBy AND ( $column == $class::$databaseColumnId OR $column == $class::$databaseColumnMap[ 'title' ] ) )
{
return 'asc';
}
return parent::getSortDirection( $column );
}
}
+16
View File
@@ -65,6 +65,16 @@ abstract class _Table
* @brief Columns that are not sortable
*/
public $noSort = array();
/**
* @brief Default column to sort results by
*/
public $defaultSortBy;
/**
* @brief Default sort direction - "asc" or "desc"
*/
public $defaultSortDirection = NULL;
/**
* @brief Filters
@@ -770,6 +780,12 @@ abstract class _Table
*/
public function getSortDirection( $column )
{
/* If the column we are sorting by is the default sort, use the default sort direction */
if ( $this->defaultSortBy AND $column == $this->defaultSortBy AND $this->defaultSortDirection )
{
return $this->defaultSortDirection;
}
if( $column == 'title' )
{
return 'asc';
+4 -3
View File
@@ -55,12 +55,13 @@ abstract class _Image
/**
* Create Object
*
* @param string $contents Contents
* @param string $contents Contents
* @param bool $checkRotation Check image to correct rotation
* @return \IPS\Image
* @throws \InvalidArgumentException
* @link https://en.wikipedia.org/wiki/List_of_file_signatures
*/
public static function create( $contents )
public static function create( string $contents, bool $checkRotation=TRUE )
{
/* Work out the type */
$imageType = NULL;
@@ -127,7 +128,7 @@ abstract class _Image
}
/* Set EXIF data immediately */
if( static::exifSupported() )
if( static::exifSupported() AND $checkRotation )
{
$obj->setExifData( $contents );
-29
View File
@@ -158,35 +158,6 @@ class _Imagemagick extends \IPS\Image
$this->setDimensions();
}
/**
* Resize to maximum
*
* @param int|NULL $maxWidth Max Width (in pixels) or NULL
* @param int|NULL $maxHeight Max Height (in pixels) or NULL
* @param bool $retainRatio If TRUE, the image will keep it's current width/height ratio rather than being squashed
* @return bool Returns TRUE if it actually resized, or FALSE if it wasn't necessary
*/
public function resizeToMax( $maxWidth=NULL, $maxHeight=NULL, $retainRatio=TRUE )
{
/* If we want to strip exif, we need to process this image regardless of size */
if( ! \IPS\Settings::i()->imagick_strip_exif )
{
return parent::resizeToMax( $maxWidth, $maxHeight, $retainRatio );
}
/* If the image is smaller, let's "resize" it keeping the same dimensions, so it runs via the __toString method */
if( ( $maxWidth == NULL or $this->width < $maxWidth ) and ( $maxHeight == NULL or $this->height < $maxHeight ) )
{
$this->resize( $this->width, $this->height );
return true;
}
else
{
return parent::resizeToMax( $maxWidth, $maxHeight, $retainRatio );
}
}
/**
* Crop to a given width and height (will attempt to downsize first)
*
+2 -2
View File
@@ -1997,11 +1997,11 @@ class _Lang extends \IPS\Node\Model
{
if( isset( $values['options']['jsonEscape'] ) )
{
$replacement = mb_substr( json_encode( $replacement, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS ), 1, -1 );
$replacement = mb_substr( json_encode( $replacement, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_UNESCAPED_UNICODE ), 1, -1 );
}
else
{
$replacement = mb_substr( json_encode( $replacement, JSON_UNESCAPED_UNICODE ), 1, -1 );
$replacement = mb_substr( json_encode( $replacement ), 1, -1 );
}
}
+13 -2
View File
@@ -191,6 +191,15 @@ class _Handler extends \IPS\MFA\MFAHandler
*/
public function toggle( $enabled )
{
if( $enabled )
{
/* Check we have some questions */
if ( !count( \IPS\MFA\SecurityQuestions\Question::roots() ) )
{
throw new \DomainException( 'no_questions' );
}
}
\IPS\Settings::i()->changeValues( array( 'security_questions_enabled' => $enabled ) );
}
@@ -222,7 +231,9 @@ class _Handler extends \IPS\MFA\MFAHandler
), NULL, NULL, NULL, 'security_questions_groups' ) );
$form->add( new \IPS\Helpers\Form\Radio( 'security_questions_prompt', \IPS\Settings::i()->security_questions_prompt, FALSE, array( 'options' => array( 'register' => 'security_questions_prompt_register', 'optional' => 'security_questions_prompt_optional', 'access' => 'security_questions_prompt_access' ) ), NULL, NULL, NULL, 'security_questions_prompt' ) );
$form->add( new \IPS\Helpers\Form\Number( 'security_questions_number', \IPS\Settings::i()->security_questions_number, FALSE, array( 'min' => 1, 'max' => \IPS\Db::i()->select( 'COUNT(*)', 'core_security_questions' )->first() ?: NULL ), NULL, NULL, NULL, 'security_questions_number' ) );
$form->addMessage('nexus_mfa_reset_answers');
if ( $values = $form->values() )
{
$values['security_questions_groups'] = ( $values['security_questions_groups'] == '*' ) ? '*' : implode( ',', $values['security_questions_groups'] );
@@ -352,7 +363,7 @@ class _Handler extends \IPS\MFA\MFAHandler
* If member has configured this handler, disable it
*
* @param \IPS\Member $member The member
* @return bool
* @return void
*/
public function disableHandlerForMember( \IPS\Member $member )
{
+26 -7
View File
@@ -728,7 +728,7 @@ class _Club extends \IPS\Patterns\ActiveRecord implements \IPS\Content\Embeddabl
}
else if( !$new AND isset( $this->changed['approved'] ) )
{
\IPS\Api\Webhook::fire( 'club_created', $this );
$this->onApprove();
}
return NULL;
@@ -1364,17 +1364,25 @@ class _Club extends \IPS\Patterns\ActiveRecord implements \IPS\Content\Embeddabl
{
continue;
}
$this->cachedNodes[ $row['id'] ] = array(
try
{
$this->cachedNodes[ $row['id'] ] = array(
'name' => $row['name'],
'url' => \IPS\Http\Url::internal( $class::$urlBase . $row['node_id'], 'front', $class::$urlTemplate, array( \IPS\Http\Url\Friendly::seoTitle( $row['name'] ) ) ),
'url' => $row['node_class']::load( $row['node_id'] )->url(),
'node_class' => $row['node_class'],
'node_id' => $row['node_id'],
'public' => $row['public']
);
);
}
catch( \OutOfRangeException $e )
{
\IPS\Log::log( 'Missing club node ' . $row['node_class'] . ' ' . $row['node_id'] . " is being loaded.", 'club_nodes');
}
}
}
return $this->cachedNodes;
}
@@ -2172,4 +2180,15 @@ class _Club extends \IPS\Patterns\ActiveRecord implements \IPS\Content\Embeddabl
\IPS\Db::i()->update( 'core_clubs_memberships', array( 'rules_acknowledged' => TRUE ), array( "club_id=? AND member_id=?", $this->id, $member->member_id ) );
}
}
/**
* Called when a club requiring moderation gets approved
*
* @return void
*/
public function onApprove()
{
\IPS\Api\Webhook::fire( 'club_created', $this );
$this->owner->achievementAction( 'core', 'NewClub', $this );
}
}
+10
View File
@@ -157,6 +157,12 @@ class _Page extends \IPS\Patterns\ActiveRecord
'moderator' => 'club_page_moderators'
) ) ) );
}
/* Add the index setting if this page is shown to guests */
if ( $club->type !== \IPS\Member\Club::TYPE_PRIVATE AND \IPS\Member::loggedIn()->group['gbw_club_manage_indexing'] )
{
$form->add( new \IPS\Helpers\Form\YesNo('club_page_meta_index', ( $current ) ? $current->meta_index : TRUE, FALSE, [], NULL, NULL, NULL, 'club_page_meta_index' ) );
}
}
/**
@@ -178,6 +184,10 @@ class _Page extends \IPS\Patterns\ActiveRecord
{
$this->can_view = NULL;
}
if( array_key_exists( 'club_page_meta_index', $values ) )
{
$this->meta_index = $values['club_page_meta_index'];
}
}
/**
+1
View File
@@ -86,6 +86,7 @@ class _Group extends \IPS\Patterns\ActiveRecord
'gbw_hide_inline_modevents' => 128, // Users in this group can see inline moderation events (where supported)
'gbw_posted_in' => 256, // Items (topics, articles) will be marked as 'posted in by this group'
'gbw_posted_in_secondary' => 512, // Items (topics, articles) will be marked as 'posted in by this group' when the group is set as a secondary group
'gbw_club_manage_indexing' => 1024, // Users in this group can manage indexing of clubs
)
)
);
+9 -5
View File
@@ -119,6 +119,7 @@ class _Member extends \IPS\Patterns\ActiveRecord
'datalayer_event_fired' => 8388608,
'new_device_email' => 16777216, // Send an email when a new device is used to log in.
'no_solved_reenage' => 33554432, // User does not want to get topic re-engagement emails
'datalayer_pii_optout' => 67108864, // User has opted in to having their PII collected by datalayer
)
)
);
@@ -202,7 +203,7 @@ class _Member extends \IPS\Patterns\ActiveRecord
if ( !static::$loggedInMember->member_id and isset( \IPS\Request::i()->cookie['ipsTimezone'] ) )
{
static::$loggedInMember->timezone = \IPS\Request::i()->cookie['ipsTimezone'];
static::$loggedInMember->timezone = \IPS\DateTime::getFixedTimezone( \IPS\Request::i()->cookie['ipsTimezone'] );
}
}
@@ -3508,7 +3509,7 @@ class _Member extends \IPS\Patterns\ActiveRecord
}
$return['reputationPoints'] = $this->pp_reputation_points;
$return['photoUrl'] = static::photoUrl( $this->_data, FALSE );
$return['photoUrlIsDefault'] = $this->pp_photo_type == 'none' or static::photoUrl( $this->_data, FALSE, FALSE, FALSE ) != $return['photoUrl'];
$return['photoUrlIsDefault'] = \in_array( $this->pp_photo_type, [ 'none', 'letter' ] ) or static::photoUrl( $this->_data, FALSE, FALSE, FALSE ) != $return['photoUrl'];
$return['coverPhotoUrl'] = $this->pp_cover_photo ? ( (string) \IPS\File::get( 'core_Profile', $this->pp_cover_photo )->url ) : '';
$return['profileUrl'] = ( $this->member_id ) ? (string) $this->url() : NULL;
if ( !$authorizedMember )
@@ -3524,7 +3525,10 @@ class _Member extends \IPS\Patterns\ActiveRecord
$return['lastPost'] = $this->member_last_post ? \IPS\DateTime::ts( $this->member_last_post )->rfc3339() : NULL;
}
$return['birthday'] = $this->bday_month ? ( $this->bday_month . '/' . $this->bday_day . ( $this->bday_year ? '/' . $this->bday_year : '' ) ) : NULL;
if( !$authorizedMember OR $authorizedMember?->member_id === $this->member_id )
{
$return['birthday'] = $this->bday_month ? ( $this->bday_month . '/' . $this->bday_day . ( $this->bday_year ? '/' . $this->bday_year : '' ) ) : NULL;
}
$return['profileViews'] = $this->members_profile_views;
$return['customFields'] = $fields;
@@ -3822,7 +3826,7 @@ class _Member extends \IPS\Patterns\ActiveRecord
*/
public function recountReputation()
{
$this->pp_reputation_points = \IPS\Db::i()->select( 'SUM(rep_rating)', 'core_reputation_index', array( 'member_received=?', $this->member_id ) );
$this->pp_reputation_points = \IPS\Db::i()->select( 'SUM(rep_rating)', 'core_reputation_index', array( 'member_received=?', $this->member_id ) )->first();
$this->save();
}
@@ -5444,7 +5448,7 @@ class _Member extends \IPS\Patterns\ActiveRecord
/* Are they enabled for our group? */
$sigLimits = explode( ":", $this->group['g_signature_limits'] );
if( $sigLimits[0] )
if( $sigLimits[0] OR $sigLimits[5] == 0 )
{
return FALSE;
}
+21
View File
@@ -199,4 +199,25 @@ class _NodeController extends \IPS\Api\Controller
/* Output */
return $node;
}
/**
* Returns the global available where condition for all nodes
*
* @param array|null $where Extra WHERE clause
* @return array
*/
protected function _globalWhere(array $where = NULL): array
{
$class = $this->class;
$where = $where ?: [];
$idField = $class::$databaseTable . '.' . $class::$databasePrefix . '.' . $class::$databaseColumnId;
if ( isset( \IPS\Request::i()->ids ) )
{
$where[] = array( \IPS\Db::i()->in( $idField, array_map( 'intval', explode(',', \IPS\Request::i()->ids ) ) ) );
}
return $where;
}
}
+11 -3
View File
@@ -3783,10 +3783,18 @@ abstract class _Model extends \IPS\Patterns\ActiveRecord
{
$return['permissions'] = \in_array( 'IPS\Node\Permissions', class_implements( \get_class( $this ) ) ) ? $this->permissions() : NULL;
}
if ( \IPS\IPS::classUsesTrait( \get_called_class(), 'IPS\Content\ClubContainer' ) AND $this->club() )
if ( \IPS\IPS::classUsesTrait( \get_called_class(), 'IPS\Content\ClubContainer' ) )
{
$return['public'] = ( $this->isPublic() ) ? 1 : 0;
if( $this->club() )
{
$return['public'] = ( $this->isPublic() ) ? 1 : 0;
$return['club'] = $this->club()->apiOutput( $authorizedMember );
}
else
{
$return['club'] = 0;
}
}
return $return;
+12 -3
View File
@@ -1103,9 +1103,18 @@ class _Notification
$headers['Content-Length'] = \Minishlink\WebPush\Utils::safeStrlen($content);
$headers['Authorization'] = $vapidHeaders['Authorization'];
// Step 8: Send the damn thing
$response = $endpoint->request()->setHeaders( $headers )->post( $content );
try
{
// Step 8: Send the damn thing
$response = $endpoint->request()->setHeaders( $headers )->post( $content );
}
catch( \IPS\Http\Request\Exception $e )
{
/* If the request failed (DNS issues, etc.). Log it and move on. */
\IPS\Log::log( $e, 'pwa_notification' );
continue;
}
// Step 9: Check the response - 404/410 indicates a permanent problem, so delete that key
if( \in_array( $response->httpResponseCode, array( 404, 410 ) ) )
+31 -1
View File
@@ -230,6 +230,11 @@ class _Output
* @brief pageName for data-pageName in the <body> tag
*/
public $pageName = NULL;
/**
* @brief Data which were loaded via the GraphQL framework, but which have to be immediately available, rather then via later AJAX requests
*/
public $graphData = [];
/**
* Constructor
@@ -259,12 +264,32 @@ class _Output
}
}
}
/**
* Get a JS bundle and add the files to the output. This greatly cleans up the usage from outside of the Output class
*
* @par JS Bundle Filename Cheatsheet
* @li library.js (this is jQuery, mustache, underscore, jstz, etc)
* @li framework.js (this is ui/, utils/*, ips.model.js, ips.controller.js and the editor controllers)
* @li admin.js or front.js (these are controllers, templates and models which are used everywhere for that location)
* @li app.js (this is all models for a single application)
* @li {location}_{section}.js (this is all controllers and templates for this section called ad-hoc when needed)
*
* @param string $file Filename
* @param string|null $app Application
* @param string|null $location Location (e.g. 'admin', 'front')
* @return array URL to JS files
*/
public function addJsFiles( $file, $app=NULL, $location=NULL )
{
$this->jsFiles = array_merge( $this->jsFiles, $this->js( $file, $app, $location ) );
}
/**
* Get a JS bundle
*
* @par JS Bundle Cheatsheet
* @li library.js (this is jQuery, mustache, underscore, jstz, etc)
* @li library.js (this is jQuery, mustache, underscore etc)
* @li framework.js (this is ui/, utils/*, ips.model.js, ips.controller.js and the editor controllers)
* @li admin.js or front.js (these are controllers, templates and models which are used everywhere for that location)
* @li app.js (this is all models for a single application)
@@ -808,6 +833,11 @@ class _Output
exit;
}
if ( \defined('LIVE_TOPICS_DEV') AND \LIVE_TOPICS_DEV )
{
$httpHeaders['Access-Control-Allow-Origin'] = '*';
}
/* Cache session Data Layer events */
if ( !( $httpStatusCode === 200 AND !\IPS\Request::i()->isAjax() AND $contentType == 'text/html' ) )
{
+14 -3
View File
@@ -756,6 +756,7 @@ CODE;
$inHereDoc = NULL;
$inThemeHooks = FALSE;
$inComments = FALSE;
$inUseStatement = FALSE;
$fh = fopen( $file, 'r' );
while ( $line = fgets( $fh ) )
@@ -805,10 +806,16 @@ CODE;
if ( $depth == 2 and $closeBraces )
{
$contents .= "{$tabs}}\n{$tabs}catch ( \Error | \RuntimeException \$e )\n{$tabs}{\n{$tabs}\tif( \defined( '\IPS\DEBUG_HOOKS' ) AND \IPS\DEBUG_HOOKS )\n{$tabs}\t{\n{$tabs}\t\t\IPS\Log::log( \$e, 'hook_exception' );\n{$tabs}\t}\n\n{$tabs}\tif ( method_exists( get_parent_class(), __FUNCTION__ ) )\n{$tabs}\t{\n{$tabs}\t\treturn \\call_user_func_array( 'parent::' . __FUNCTION__, \\func_get_args() );\n{$tabs}\t}\n{$tabs}\telse\n{$tabs}\t{\n{$tabs}\t\tthrow \$e;\n{$tabs}\t}\n{$tabs}}\n";
/* If we are in a USE statement, we don't want to do any wrapping */
if ( !$inUseStatement )
{
$contents .= "{$tabs}}\n{$tabs}catch ( \Error | \RuntimeException \$e )\n{$tabs}{\n{$tabs}\tif( \defined( '\IPS\DEBUG_HOOKS' ) AND \IPS\DEBUG_HOOKS )\n{$tabs}\t{\n{$tabs}\t\t\IPS\Log::log( \$e, 'hook_exception' );\n{$tabs}\t}\n\n{$tabs}\tif ( method_exists( get_parent_class(), __FUNCTION__ ) )\n{$tabs}\t{\n{$tabs}\t\treturn \\call_user_func_array( 'parent::' . __FUNCTION__, \\func_get_args() );\n{$tabs}\t}\n{$tabs}\telse\n{$tabs}\t{\n{$tabs}\t\tthrow \$e;\n{$tabs}\t}\n{$tabs}}\n";
}
$inUseStatement = FALSE;
$depth--;
}
if ( !$inHereDoc and $depth > 2 )
{
$contents .= "\t";
@@ -825,7 +832,11 @@ CODE;
if ( $depth == 2 and $openBraces )
{
$contents .= "{$tabs}try\n{$tabs}{\n";
$inUseStatement = \substr( \trim( \strtolower( $line ) ), 0, 3 ) == 'use';
if ( !$inUseStatement )
{
$contents .= "{$tabs}try\n{$tabs}{\n";
}
$depth++;
}
+23 -2
View File
@@ -213,7 +213,7 @@ class _Poll extends \IPS\Patterns\ActiveRecord implements \SplSubject
{
$choices[ $k ] = array(
'question' => $questionData['title'],
'multi' => \intval( isset( $questionData['multichoice'] ) ),
'multi' => (int) !empty( $questionData['multichoice'] ),
'choice' => array(),
'votes' => array(),
);
@@ -588,7 +588,7 @@ class _Poll extends \IPS\Patterns\ActiveRecord implements \SplSubject
foreach ( $this->choices as $k => $data )
{
$class = ( isset( $data['multi'] ) AND $data['multi'] ) ? 'IPS\Helpers\Form\CheckboxSet' : 'IPS\Helpers\Form\Radio';
$input = new $class( $k, ( isset( $data['multi'] ) AND $data['multi'] ) ? array() : NULL, TRUE, array( 'options' => $data['choice'], 'noDefault' => TRUE ) );
$input = new $class( $k, ( isset( $data['multi'] ) AND $data['multi'] ) ? array() : NULL, TRUE, array( 'options' => $data['choice'], 'noDefault' => TRUE, 'showAllNone' => FALSE, 'condense' => FALSE ) );
$input->label = $data['question'];
$form->add( $input );
@@ -660,6 +660,10 @@ class _Poll extends \IPS\Patterns\ActiveRecord implements \SplSubject
* @return array
* @apiresponse int id ID number
* @apiresponse string title Title
* @apiresponse datetime startDate Start Date
* @apiresponse bool closed Closed
* @apiresponse datetime closedDate Closed Date
* @apiresponse bool public Public poll
* @apiresponse int votes Number of votes
* @apiresponse [\IPS\Poll\Question] questions The questions
*/
@@ -674,6 +678,10 @@ class _Poll extends \IPS\Patterns\ActiveRecord implements \SplSubject
return array(
'id' => $this->pid,
'title' => $this->poll_question,
'startDate' => $this->start_date->rfc3339(),
'closed' => (bool) $this->poll_closed,
'closedDate' => ( $this->poll_closed ? $this->poll_close_date->rfc3339() : null ),
'public' => (bool) $this->poll_view_voters,
'votes' => $this->votes,
'questions' => $questions
);
@@ -699,4 +707,17 @@ class _Poll extends \IPS\Patterns\ActiveRecord implements \SplSubject
$this->author()->achievementAction( 'core', 'NewPoll', $this );
}
}
/**
* [ActiveRecord] Delete Record
*
* @return void
*/
public function delete()
{
parent::delete();
/* Delete records from voters table */
\IPS\Db::i()->delete( 'core_voters', array( 'poll=?', $this->pid ) );
}
}
+6 -6
View File
@@ -724,13 +724,13 @@ class _Request extends \IPS\Patterns\Singleton
if ( \function_exists('apache_request_headers') )
{
$headers = @apache_request_headers();
if ( isset( $headers['Authorization'] ) )
$headerKeys = ['authorization', 'x-authorization'];
foreach ( $headers as $k => $v )
{
return $headers['Authorization'];
}
elseif ( isset( $headers['X-Authorization'] ) )
{
return $headers['X-Authorization'];
if ( \in_array( \mb_strtolower( $k ), $headerKeys ) )
{
return $v;
}
}
}
+1 -1
View File
@@ -160,7 +160,7 @@ class _Front extends \IPS\Session
{
$accessToken = \IPS\Api\OAuthClient::accessTokenDetails( mb_substr( $authorizationHeader, 7 ) );
$client = \IPS\Api\OAuthClient::load( $accessToken['client_id'] );
if ( $client->graphql and $accessToken['member_id'] === $expectedMember->member_id )
if ( \in_array($client->api_access, [ 'graphql', 'both'] ) AND $accessToken['member_id'] === $expectedMember->member_id )
{
$success = TRUE;
}
+2 -1
View File
@@ -24,9 +24,10 @@ class _Sitemap
{
/**
* @brief Maximum number of entries to include per file
* @deprecated 4.7.8 This will be removed in the future. Use now \IPS\SITEMAP_MAX_PER_FILE
*/
const MAX_PER_FILE = 500;
/**
* @brief Count options
*/
+1 -1
View File
@@ -1930,7 +1930,7 @@ class _Parser
$file = $this->_getFile( 'core_Attachment', $element->getAttribute( $srcAttribute ) );
$element->setAttribute( 'data-fileid', $attachment['attach_id'] );
$element->setAttribute( 'src', str_replace( array( 'http:', 'https:' ), '', str_replace( static::$fileObjectClasses['core_Attachment']->baseUrl(), '{fileStore.core_Attachment}', $element->getAttribute( $srcAttribute ) ) ) );
$element->setAttribute( $srcAttribute, str_replace( array( 'http:', 'https:' ), '', str_replace( static::$fileObjectClasses['core_Attachment']->baseUrl(), '{fileStore.core_Attachment}', $element->getAttribute( $srcAttribute ) ) ) );
if ( ( !$element->hasAttribute('alt') or $element->getAttribute('alt') == $file->filename ) and ! empty( $attachment['attach_labels'] ) and $labels = static::getAttachmentLabels( $attachment ) )
+53
View File
@@ -0,0 +1,53 @@
<?php
/**
* @brief Theme Hook Compilation Skeleton - Catch calls to parent during compile.
* @author <a href='https://invisioncommunity.com/'>Invision Power Services, Inc.</a>
* @copyright (c) 2023 Invision Power Services, Inc.
* @package Invision Community
* @since 16 February 2023
*/
namespace IPS\Theme\Compile;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Theme Hook skeleton class
*/
class _ThemeHook
{
/**
* @param $name
* @param $arguments
* @return string
*/
public function __call( string $name, array $arguments ): string
{
return '';
}
/**
* @param $name
* @param $arguments
* @return string
*/
public static function __callStatic( string $name, array $arguments ): string
{
return '';
}
/**
* @param $name
* @return void
*/
public function __set( string $name, mixed $value ): void
{
}
}
+1 -1
View File
@@ -2700,7 +2700,7 @@ class _Theme extends \IPS\Node\Model
{
try
{
if ( eval( "namespace IPS\\Theme;\n\n" . str_replace( array( ' extends _HOOK_CLASS_', 'parent::hookData()' ), array( '_tmp', 'array()' ), file_get_contents( $path . '/' . $data['file'] ) ) ) !== FALSE )
if ( eval( "namespace IPS\\Theme;\n\n" . str_replace( array( ' extends _HOOK_CLASS_', 'parent::hookData()' ), array( '_tmp EXTENDS \IPS\Theme\Compile\ThemeHook', 'array()' ), file_get_contents( $path . '/' . $data['file'] ) ) ) !== FALSE )
{
$class = "IPS\\Theme\\" . $data['class'] . "_tmp";
$templateHooks = array_merge_recursive( $templateHooks, $class::hookData() );