Version 4.7.0

This commit is contained in:
Neo committed 2025-12-19 16:00:35 -08:00
1 parent 517a5e1f70
commit 8ba7d43898
1933 files changed
+65613 -11332

No files matched your search

@@ -4,12 +4,11 @@
* @file
* Legacy autoloader for systems lacking spl_autoload_register
*
* Must be separate to prevent deprecation warning on PHP 7.2
*/
function __autoload($class)
spl_autoload_register(function($class)
{
return HTMLPurifier_Bootstrap::autoload($class);
}
return HTMLPurifier_Bootstrap::autoload($class);
});
// vim: et sw=4 sts=4
@@ -17,6 +17,7 @@ if (function_exists('spl_autoload_register') && function_exists('spl_autoload_un
require dirname(__FILE__) . '/HTMLPurifier.autoload-legacy.php';
}
// phpcs:ignore PHPCompatibility.IniDirectives.RemovedIniDirectives.zend_ze1_compatibility_modeRemoved
if (ini_get('zend.ze1_compatibility_mode')) {
trigger_error("HTML Purifier is not compatible with zend.ze1_compatibility_mode; please turn it off", E_USER_ERROR);
}
+1 -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.12.0
* @version 4.14.0
*
* @warning
* You must *not* include any other HTML Purifier files before this file,
+4 -3
View File
@@ -19,7 +19,7 @@
*/
/*
HTML Purifier 4.12.0 - Standards Compliant HTML Filtering
HTML Purifier 4.14.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.12.0';
public $version = '4.14.0';
/**
* Constant with version of HTML Purifier.
*/
const VERSION = '4.12.0';
const VERSION = '4.14.0';
/**
* Global configuration object.
@@ -240,6 +240,7 @@ class HTMLPurifier
public function purifyArray($array_of_html, $config = null)
{
$context_array = array();
$array = array();
foreach($array_of_html as $key=>$value){
if (is_array($value)) {
$array[$key] = $this->purifyArray($value, $config);
@@ -25,6 +25,7 @@ class HTMLPurifier_AttrDef_CSS_Background extends HTMLPurifier_AttrDef
$this->info['background-repeat'] = $def->info['background-repeat'];
$this->info['background-attachment'] = $def->info['background-attachment'];
$this->info['background-position'] = $def->info['background-position'];
$this->info['background-size'] = $def->info['background-size'];
}
/**
@@ -53,6 +54,7 @@ class HTMLPurifier_AttrDef_CSS_Background extends HTMLPurifier_AttrDef
$caught['repeat'] = false;
$caught['attachment'] = false;
$caught['position'] = false;
$caught['size'] = false;
$i = 0; // number of catches
@@ -69,7 +69,13 @@ class HTMLPurifier_AttrDef_CSS_Number extends HTMLPurifier_AttrDef
return false;
}
$left = ltrim($left, '0');
// Remove leading zeros until positive number or a zero stays left
if (ltrim($left, '0') != '') {
$left = ltrim($left, '0');
} else {
$left = '0';
}
$right = rtrim($right, '0');
if ($right === '') {
@@ -109,6 +109,22 @@ class HTMLPurifier_CSSDefinition extends HTMLPurifier_Definition
);
$this->info['background-position'] = new HTMLPurifier_AttrDef_CSS_BackgroundPosition();
$this->info['background-size'] = new HTMLPurifier_AttrDef_CSS_Composite(
array(
new HTMLPurifier_AttrDef_Enum(
array(
'auto',
'cover',
'contain',
'initial',
'inherit',
)
),
new HTMLPurifier_AttrDef_CSS_Percentage(),
new HTMLPurifier_AttrDef_CSS_Length()
)
);
$border_color =
$this->info['border-top-color'] =
$this->info['border-bottom-color'] =
@@ -164,7 +164,7 @@ class HTMLPurifier_ChildDef_Table extends HTMLPurifier_ChildDef
}
}
if (empty($content)) {
if (empty($content) && $thead === false && $tfoot === false) {
return false;
}
+9 -9
View File
@@ -21,7 +21,7 @@ class HTMLPurifier_Config
* HTML Purifier's version
* @type string
*/
public $version = '4.12.0';
public $version = '4.14.0';
/**
* Whether or not to automatically finalize
@@ -408,7 +408,7 @@ class HTMLPurifier_Config
* maybeGetRawHTMLDefinition, which is more explicitly
* named, instead.
*
* @return HTMLPurifier_HTMLDefinition
* @return HTMLPurifier_HTMLDefinition|null
*/
public function getHTMLDefinition($raw = false, $optimized = false)
{
@@ -427,7 +427,7 @@ class HTMLPurifier_Config
* maybeGetRawCSSDefinition, which is more explicitly
* named, instead.
*
* @return HTMLPurifier_CSSDefinition
* @return HTMLPurifier_CSSDefinition|null
*/
public function getCSSDefinition($raw = false, $optimized = false)
{
@@ -446,7 +446,7 @@ class HTMLPurifier_Config
* maybeGetRawURIDefinition, which is more explicitly
* named, instead.
*
* @return HTMLPurifier_URIDefinition
* @return HTMLPurifier_URIDefinition|null
*/
public function getURIDefinition($raw = false, $optimized = false)
{
@@ -468,7 +468,7 @@ class HTMLPurifier_Config
* maybe semantics is the "right thing to do."
*
* @throws HTMLPurifier_Exception
* @return HTMLPurifier_Definition
* @return HTMLPurifier_Definition|null
*/
public function getDefinition($type, $raw = false, $optimized = false)
{
@@ -647,7 +647,7 @@ class HTMLPurifier_Config
}
/**
* @return HTMLPurifier_HTMLDefinition
* @return HTMLPurifier_HTMLDefinition|null
*/
public function maybeGetRawHTMLDefinition()
{
@@ -655,7 +655,7 @@ class HTMLPurifier_Config
}
/**
* @return HTMLPurifier_CSSDefinition
* @return HTMLPurifier_CSSDefinition|null
*/
public function maybeGetRawCSSDefinition()
{
@@ -663,7 +663,7 @@ class HTMLPurifier_Config
}
/**
* @return HTMLPurifier_URIDefinition
* @return HTMLPurifier_URIDefinition|null
*/
public function maybeGetRawURIDefinition()
{
@@ -803,7 +803,7 @@ class HTMLPurifier_Config
if ($index !== false) {
$array = (isset($array[$index]) && is_array($array[$index])) ? $array[$index] : array();
}
$mq = $mq_fix && function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc();
$mq = $mq_fix && version_compare(PHP_VERSION, '7.4.0', '<') && function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc();
$allowed = HTMLPurifier_Config::getAllowedDirectivesForForm($allowed, $schema);
$ret = array();
@@ -1 +1 @@
O:25:"HTMLPurifier_ConfigSchema":3:{s:8:"defaults";a:126:{s:19:"Attr.AllowedClasses";N;s:24:"Attr.AllowedFrameTargets";a:0:{}s:15:"Attr.AllowedRel";a:0:{}s:15:"Attr.AllowedRev";a:0:{}s:18:"Attr.ClassUseCDATA";N;s:20:"Attr.DefaultImageAlt";N;s:24:"Attr.DefaultInvalidImage";s:0:"";s:27:"Attr.DefaultInvalidImageAlt";s:13:"Invalid image";s:19:"Attr.DefaultTextDir";s:3:"ltr";s:13:"Attr.EnableID";b:0;s:21:"Attr.ForbiddenClasses";a:0:{}s:13:"Attr.ID.HTML5";N;s:16:"Attr.IDBlacklist";a:0:{}s:22:"Attr.IDBlacklistRegexp";N;s:13:"Attr.IDPrefix";s:0:"";s:18:"Attr.IDPrefixLocal";s:0:"";s:24:"AutoFormat.AutoParagraph";b:0;s:17:"AutoFormat.Custom";a:0:{}s:25:"AutoFormat.DisplayLinkURI";b:0;s:18:"AutoFormat.Linkify";b:0;s:33:"AutoFormat.PurifierLinkify.DocURL";s:3:"#%s";s:26:"AutoFormat.PurifierLinkify";b:0;s:32:"AutoFormat.RemoveEmpty.Predicate";a:4:{s:8:"colgroup";a:0:{}s:2:"th";a:0:{}s:2:"td";a:0:{}s:6:"iframe";a:1:{i:0;s:3:"src";}}s:44:"AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions";a:2:{s:2:"td";b:1;s:2:"th";b:1;}s:33:"AutoFormat.RemoveEmpty.RemoveNbsp";b:0;s:22:"AutoFormat.RemoveEmpty";b:0;s:39:"AutoFormat.RemoveSpansWithoutAttributes";b:0;s:19:"CSS.AllowDuplicates";b:0;s:18:"CSS.AllowImportant";b:0;s:15:"CSS.AllowTricky";b:0;s:16:"CSS.AllowedFonts";N;s:21:"CSS.AllowedProperties";N;s:17:"CSS.DefinitionRev";i:1;s:23:"CSS.ForbiddenProperties";a:0:{}s:16:"CSS.MaxImgLength";s:6:"1200px";s:15:"CSS.Proprietary";b:0;s:11:"CSS.Trusted";b:0;s:20:"Cache.DefinitionImpl";s:10:"Serializer";s:20:"Cache.SerializerPath";N;s:27:"Cache.SerializerPermissions";i:493;s:22:"Core.AggressivelyFixLt";b:1;s:29:"Core.AggressivelyRemoveScript";b:1;s:28:"Core.AllowHostnameUnderscore";b:0;s:23:"Core.AllowParseManyTags";b:0;s:18:"Core.CollectErrors";b:0;s:18:"Core.ColorKeywords";a:148:{s:9:"aliceblue";s:7:"#F0F8FF";s:12:"antiquewhite";s:7:"#FAEBD7";s:4:"aqua";s:7:"#00FFFF";s:10:"aquamarine";s:7:"#7FFFD4";s:5:"azure";s:7:"#F0FFFF";s:5:"beige";s:7:"#F5F5DC";s:6:"bisque";s:7:"#FFE4C4";s:5:"black";s:7:"#000000";s:14:"blanchedalmond";s:7:"#FFEBCD";s:4:"blue";s:7:"#0000FF";s:10:"blueviolet";s:7:"#8A2BE2";s:5:"brown";s:7:"#A52A2A";s:9:"burlywood";s:7:"#DEB887";s:9:"cadetblue";s:7:"#5F9EA0";s:10:"chartreuse";s:7:"#7FFF00";s:9:"chocolate";s:7:"#D2691E";s:5:"coral";s:7:"#FF7F50";s:14:"cornflowerblue";s:7:"#6495ED";s:8:"cornsilk";s:7:"#FFF8DC";s:7:"crimson";s:7:"#DC143C";s:4:"cyan";s:7:"#00FFFF";s:8:"darkblue";s:7:"#00008B";s:8:"darkcyan";s:7:"#008B8B";s:13:"darkgoldenrod";s:7:"#B8860B";s:8:"darkgray";s:7:"#A9A9A9";s:8:"darkgrey";s:7:"#A9A9A9";s:9:"darkgreen";s:7:"#006400";s:9:"darkkhaki";s:7:"#BDB76B";s:11:"darkmagenta";s:7:"#8B008B";s:14:"darkolivegreen";s:7:"#556B2F";s:10:"darkorange";s:7:"#FF8C00";s:10:"darkorchid";s:7:"#9932CC";s:7:"darkred";s:7:"#8B0000";s:10:"darksalmon";s:7:"#E9967A";s:12:"darkseagreen";s:7:"#8FBC8F";s:13:"darkslateblue";s:7:"#483D8B";s:13:"darkslategray";s:7:"#2F4F4F";s:13:"darkslategrey";s:7:"#2F4F4F";s:13:"darkturquoise";s:7:"#00CED1";s:10:"darkviolet";s:7:"#9400D3";s:8:"deeppink";s:7:"#FF1493";s:11:"deepskyblue";s:7:"#00BFFF";s:7:"dimgray";s:7:"#696969";s:7:"dimgrey";s:7:"#696969";s:10:"dodgerblue";s:7:"#1E90FF";s:9:"firebrick";s:7:"#B22222";s:11:"floralwhite";s:7:"#FFFAF0";s:11:"forestgreen";s:7:"#228B22";s:7:"fuchsia";s:7:"#FF00FF";s:9:"gainsboro";s:7:"#DCDCDC";s:10:"ghostwhite";s:7:"#F8F8FF";s:4:"gold";s:7:"#FFD700";s:9:"goldenrod";s:7:"#DAA520";s:4:"gray";s:7:"#808080";s:4:"grey";s:7:"#808080";s:5:"green";s:7:"#008000";s:11:"greenyellow";s:7:"#ADFF2F";s:8:"honeydew";s:7:"#F0FFF0";s:7:"hotpink";s:7:"#FF69B4";s:9:"indianred";s:7:"#CD5C5C";s:6:"indigo";s:7:"#4B0082";s:5:"ivory";s:7:"#FFFFF0";s:5:"khaki";s:7:"#F0E68C";s:8:"lavender";s:7:"#E6E6FA";s:13:"lavenderblush";s:7:"#FFF0F5";s:9:"lawngreen";s:7:"#7CFC00";s:12:"lemonchiffon";s:7:"#FFFACD";s:9:"lightblue";s:7:"#ADD8E6";s:10:"lightcoral";s:7:"#F08080";s:9:"lightcyan";s:7:"#E0FFFF";s:20:"lightgoldenrodyellow";s:7:"#FAFAD2";s:9:"lightgray";s:7:"#D3D3D3";s:9:"lightgrey";s:7:"#D3D3D3";s:10:"lightgreen";s:7:"#90EE90";s:9:"lightpink";s:7:"#FFB6C1";s:11:"lightsalmon";s:7:"#FFA07A";s:13:"lightseagreen";s:7:"#20B2AA";s:12:"lightskyblue";s:7:"#87CEFA";s:14:"lightslategray";s:7:"#778899";s:14:"lightslategrey";s:7:"#778899";s:14:"lightsteelblue";s:7:"#B0C4DE";s:11:"lightyellow";s:7:"#FFFFE0";s:4:"lime";s:7:"#00FF00";s:9:"limegreen";s:7:"#32CD32";s:5:"linen";s:7:"#FAF0E6";s:7:"magenta";s:7:"#FF00FF";s:6:"maroon";s:7:"#800000";s:16:"mediumaquamarine";s:7:"#66CDAA";s:10:"mediumblue";s:7:"#0000CD";s:12:"mediumorchid";s:7:"#BA55D3";s:12:"mediumpurple";s:7:"#9370DB";s:14:"mediumseagreen";s:7:"#3CB371";s:15:"mediumslateblue";s:7:"#7B68EE";s:17:"mediumspringgreen";s:7:"#00FA9A";s:15:"mediumturquoise";s:7:"#48D1CC";s:15:"mediumvioletred";s:7:"#C71585";s:12:"midnightblue";s:7:"#191970";s:9:"mintcream";s:7:"#F5FFFA";s:9:"mistyrose";s:7:"#FFE4E1";s:8:"moccasin";s:7:"#FFE4B5";s:11:"navajowhite";s:7:"#FFDEAD";s:4:"navy";s:7:"#000080";s:7:"oldlace";s:7:"#FDF5E6";s:5:"olive";s:7:"#808000";s:9:"olivedrab";Line truncated
O:25:"HTMLPurifier_ConfigSchema":3:{s:8:"defaults";a:127:{s:19:"Attr.AllowedClasses";N;s:24:"Attr.AllowedFrameTargets";a:0:{}s:15:"Attr.AllowedRel";a:0:{}s:15:"Attr.AllowedRev";a:0:{}s:18:"Attr.ClassUseCDATA";N;s:20:"Attr.DefaultImageAlt";N;s:24:"Attr.DefaultInvalidImage";s:0:"";s:27:"Attr.DefaultInvalidImageAlt";s:13:"Invalid image";s:19:"Attr.DefaultTextDir";s:3:"ltr";s:13:"Attr.EnableID";b:0;s:21:"Attr.ForbiddenClasses";a:0:{}s:13:"Attr.ID.HTML5";N;s:16:"Attr.IDBlacklist";a:0:{}s:22:"Attr.IDBlacklistRegexp";N;s:13:"Attr.IDPrefix";s:0:"";s:18:"Attr.IDPrefixLocal";s:0:"";s:24:"AutoFormat.AutoParagraph";b:0;s:17:"AutoFormat.Custom";a:0:{}s:25:"AutoFormat.DisplayLinkURI";b:0;s:18:"AutoFormat.Linkify";b:0;s:33:"AutoFormat.PurifierLinkify.DocURL";s:3:"#%s";s:26:"AutoFormat.PurifierLinkify";b:0;s:32:"AutoFormat.RemoveEmpty.Predicate";a:4:{s:8:"colgroup";a:0:{}s:2:"th";a:0:{}s:2:"td";a:0:{}s:6:"iframe";a:1:{i:0;s:3:"src";}}s:44:"AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions";a:2:{s:2:"td";b:1;s:2:"th";b:1;}s:33:"AutoFormat.RemoveEmpty.RemoveNbsp";b:0;s:22:"AutoFormat.RemoveEmpty";b:0;s:39:"AutoFormat.RemoveSpansWithoutAttributes";b:0;s:19:"CSS.AllowDuplicates";b:0;s:18:"CSS.AllowImportant";b:0;s:15:"CSS.AllowTricky";b:0;s:16:"CSS.AllowedFonts";N;s:21:"CSS.AllowedProperties";N;s:17:"CSS.DefinitionRev";i:1;s:23:"CSS.ForbiddenProperties";a:0:{}s:16:"CSS.MaxImgLength";s:6:"1200px";s:15:"CSS.Proprietary";b:0;s:11:"CSS.Trusted";b:0;s:20:"Cache.DefinitionImpl";s:10:"Serializer";s:20:"Cache.SerializerPath";N;s:27:"Cache.SerializerPermissions";i:493;s:22:"Core.AggressivelyFixLt";b:1;s:29:"Core.AggressivelyRemoveScript";b:1;s:28:"Core.AllowHostnameUnderscore";b:0;s:23:"Core.AllowParseManyTags";b:0;s:18:"Core.CollectErrors";b:0;s:18:"Core.ColorKeywords";a:148:{s:9:"aliceblue";s:7:"#F0F8FF";s:12:"antiquewhite";s:7:"#FAEBD7";s:4:"aqua";s:7:"#00FFFF";s:10:"aquamarine";s:7:"#7FFFD4";s:5:"azure";s:7:"#F0FFFF";s:5:"beige";s:7:"#F5F5DC";s:6:"bisque";s:7:"#FFE4C4";s:5:"black";s:7:"#000000";s:14:"blanchedalmond";s:7:"#FFEBCD";s:4:"blue";s:7:"#0000FF";s:10:"blueviolet";s:7:"#8A2BE2";s:5:"brown";s:7:"#A52A2A";s:9:"burlywood";s:7:"#DEB887";s:9:"cadetblue";s:7:"#5F9EA0";s:10:"chartreuse";s:7:"#7FFF00";s:9:"chocolate";s:7:"#D2691E";s:5:"coral";s:7:"#FF7F50";s:14:"cornflowerblue";s:7:"#6495ED";s:8:"cornsilk";s:7:"#FFF8DC";s:7:"crimson";s:7:"#DC143C";s:4:"cyan";s:7:"#00FFFF";s:8:"darkblue";s:7:"#00008B";s:8:"darkcyan";s:7:"#008B8B";s:13:"darkgoldenrod";s:7:"#B8860B";s:8:"darkgray";s:7:"#A9A9A9";s:8:"darkgrey";s:7:"#A9A9A9";s:9:"darkgreen";s:7:"#006400";s:9:"darkkhaki";s:7:"#BDB76B";s:11:"darkmagenta";s:7:"#8B008B";s:14:"darkolivegreen";s:7:"#556B2F";s:10:"darkorange";s:7:"#FF8C00";s:10:"darkorchid";s:7:"#9932CC";s:7:"darkred";s:7:"#8B0000";s:10:"darksalmon";s:7:"#E9967A";s:12:"darkseagreen";s:7:"#8FBC8F";s:13:"darkslateblue";s:7:"#483D8B";s:13:"darkslategray";s:7:"#2F4F4F";s:13:"darkslategrey";s:7:"#2F4F4F";s:13:"darkturquoise";s:7:"#00CED1";s:10:"darkviolet";s:7:"#9400D3";s:8:"deeppink";s:7:"#FF1493";s:11:"deepskyblue";s:7:"#00BFFF";s:7:"dimgray";s:7:"#696969";s:7:"dimgrey";s:7:"#696969";s:10:"dodgerblue";s:7:"#1E90FF";s:9:"firebrick";s:7:"#B22222";s:11:"floralwhite";s:7:"#FFFAF0";s:11:"forestgreen";s:7:"#228B22";s:7:"fuchsia";s:7:"#FF00FF";s:9:"gainsboro";s:7:"#DCDCDC";s:10:"ghostwhite";s:7:"#F8F8FF";s:4:"gold";s:7:"#FFD700";s:9:"goldenrod";s:7:"#DAA520";s:4:"gray";s:7:"#808080";s:4:"grey";s:7:"#808080";s:5:"green";s:7:"#008000";s:11:"greenyellow";s:7:"#ADFF2F";s:8:"honeydew";s:7:"#F0FFF0";s:7:"hotpink";s:7:"#FF69B4";s:9:"indianred";s:7:"#CD5C5C";s:6:"indigo";s:7:"#4B0082";s:5:"ivory";s:7:"#FFFFF0";s:5:"khaki";s:7:"#F0E68C";s:8:"lavender";s:7:"#E6E6FA";s:13:"lavenderblush";s:7:"#FFF0F5";s:9:"lawngreen";s:7:"#7CFC00";s:12:"lemonchiffon";s:7:"#FFFACD";s:9:"lightblue";s:7:"#ADD8E6";s:10:"lightcoral";s:7:"#F08080";s:9:"lightcyan";s:7:"#E0FFFF";s:20:"lightgoldenrodyellow";s:7:"#FAFAD2";s:9:"lightgray";s:7:"#D3D3D3";s:9:"lightgrey";s:7:"#D3D3D3";s:10:"lightgreen";s:7:"#90EE90";s:9:"lightpink";s:7:"#FFB6C1";s:11:"lightsalmon";s:7:"#FFA07A";s:13:"lightseagreen";s:7:"#20B2AA";s:12:"lightskyblue";s:7:"#87CEFA";s:14:"lightslategray";s:7:"#778899";s:14:"lightslategrey";s:7:"#778899";s:14:"lightsteelblue";s:7:"#B0C4DE";s:11:"lightyellow";s:7:"#FFFFE0";s:4:"lime";s:7:"#00FF00";s:9:"limegreen";s:7:"#32CD32";s:5:"linen";s:7:"#FAF0E6";s:7:"magenta";s:7:"#FF00FF";s:6:"maroon";s:7:"#800000";s:16:"mediumaquamarine";s:7:"#66CDAA";s:10:"mediumblue";s:7:"#0000CD";s:12:"mediumorchid";s:7:"#BA55D3";s:12:"mediumpurple";s:7:"#9370DB";s:14:"mediumseagreen";s:7:"#3CB371";s:15:"mediumslateblue";s:7:"#7B68EE";s:17:"mediumspringgreen";s:7:"#00FA9A";s:15:"mediumturquoise";s:7:"#48D1CC";s:15:"mediumvioletred";s:7:"#C71585";s:12:"midnightblue";s:7:"#191970";s:9:"mintcream";s:7:"#F5FFFA";s:9:"mistyrose";s:7:"#FFE4E1";s:8:"moccasin";s:7:"#FFE4B5";s:11:"navajowhite";s:7:"#FFDEAD";s:4:"navy";s:7:"#000080";s:7:"oldlace";s:7:"#FDF5E6";s:5:"olive";s:7:"#808000";s:9:"olivedrab";Line truncated
@@ -0,0 +1,11 @@
HTML.Forms
TYPE: bool
VERSION: 4.13.0
DEFAULT: false
--DESCRIPTION--
<p>
Whether or not to permit form elements in the user input, regardless of
%HTML.Trusted value. Please be very careful when using this functionality, as
enabling forms in untrusted documents may allow for phishing attacks.
</p>
--# vim: et sw=4 sts=4
+2 -1
View File
@@ -257,8 +257,9 @@ class HTMLPurifier_HTMLModule
*/
public function makeLookup($list)
{
$args = func_get_args();
if (is_string($list)) {
$list = func_get_args();
$list = $args;
}
$ret = array();
foreach ($list as $value) {
@@ -28,6 +28,10 @@ class HTMLPurifier_HTMLModule_Forms extends HTMLPurifier_HTMLModule
*/
public function setup($config)
{
if ($config->get('HTML.Forms')) {
$this->safe = true;
}
$form = $this->addElement(
'form',
'Form',
@@ -146,10 +146,7 @@ class HTMLPurifier_HTMLModule_Tidy extends HTMLPurifier_HTMLModule
$type = "info_$type";
$e = $this;
}
// PHP does some weird parsing when I do
// $e->$type[$attr], so I have to assign a ref.
$f =& $e->$type;
$f[$attr] = $fix;
$e->{$type}[$attr] = $fix;
break;
case 'tag_transform':
$this->info_tag_transform[$params['element']] = $fix;
@@ -96,6 +96,7 @@ class HTMLPurifier_HTMLModule_Tidy_XHTMLAndHTML4 extends HTMLPurifier_HTMLModule
// @bgcolor for table, tr, td, th ---------------------------------
$r['table@bgcolor'] =
$r['tr@bgcolor'] =
$r['td@bgcolor'] =
$r['th@bgcolor'] =
new HTMLPurifier_AttrTransform_BgColor();
@@ -167,9 +168,11 @@ class HTMLPurifier_HTMLModule_Tidy_XHTMLAndHTML4 extends HTMLPurifier_HTMLModule
// @vspace for img ------------------------------------------------
$r['img@vspace'] = new HTMLPurifier_AttrTransform_ImgSpace('vspace');
// @width for hr, td, th ------------------------------------------
// @width for table, hr, td, th, col ------------------------------------------
$r['table@width'] =
$r['td@width'] =
$r['th@width'] =
$r['col@width'] =
$r['hr@width'] = new HTMLPurifier_AttrTransform_Length('width');
return $r;
@@ -40,6 +40,9 @@ class HTMLPurifier_Injector_Linkify extends HTMLPurifier_Injector
'/\\b((?:[a-z][\\w\\-]+:(?:\\/{1,3}|[a-z0-9%])|www\\d{0,3}[.]|[a-z0-9.\\-]+[.][a-z]{2,4}\\/)(?:[^\\s()<>]|\\((?:[^\\s()<>]|(?:\\([^\\s()<>]+\\)))*\\))+(?:\\((?:[^\\s()<>]|(?:\\([^\\s()<>]+\\)))*\\)|[^\\s`!()\\[\\]{};:\'".,<>?\x{00ab}\x{00bb}\x{201c}\x{201d}\x{2018}\x{2019}]))/iu',
$token->data, -1, PREG_SPLIT_DELIM_CAPTURE);
if ($bits === false) {
return;
}
$token = array();
@@ -43,8 +43,8 @@ class HTMLPurifier_Printer_HTMLDefinition extends HTMLPurifier_Printer
$ret .= $this->element('caption', 'Doctype');
$ret .= $this->row('Name', $doctype->name);
$ret .= $this->row('XML', $doctype->xml ? 'Yes' : 'No');
$ret .= $this->row('Default Modules', implode($doctype->modules, ', '));
$ret .= $this->row('Default Tidy Modules', implode($doctype->tidyModules, ', '));
$ret .= $this->row('Default Modules', implode(', ', $doctype->modules));
$ret .= $this->row('Default Tidy Modules', implode(', ', $doctype->tidyModules));
$ret .= $this->end('table');
return $ret;
}
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2019 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Signature\Algorithm;
use Base64Url\Base64Url;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
abstract class HMAC implements MacAlgorithm
{
public function allowedKeyTypes(): array
{
return ['oct'];
}
public function verify(JWK $key, string $input, string $signature): bool
{
return hash_equals($this->hash($key, $input), $signature);
}
public function hash(JWK $key, string $input): string
{
$k = $this->getKey($key);
return hash_hmac($this->getHashAlgorithm(), $input, $k, true);
}
protected function getKey(JWK $key): string
{
if (!\in_array($key->get('kty'), $this->allowedKeyTypes(), true)) {
throw new InvalidArgumentException('Wrong key type.');
}
if (!$key->has('k')) {
throw new InvalidArgumentException('The key parameter "k" is missing.');
}
$k = $key->get('k');
if (!\is_string($k)) {
throw new InvalidArgumentException('The key parameter "k" is invalid.');
}
return Base64Url::decode($k);
}
abstract protected function getHashAlgorithm(): string;
}
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2019 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Signature\Algorithm;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
final class HS256 extends HMAC
{
public function name(): string
{
return 'HS256';
}
protected function getHashAlgorithm(): string
{
return 'sha256';
}
protected function getKey(JWK $key): string
{
$k = parent::getKey($key);
if (mb_strlen($k, '8bit') < 32) {
throw new InvalidArgumentException('Invalid key length.');
}
return $k;
}
}
+12 -4
View File
@@ -212,9 +212,8 @@
// Note that no real action is taken, if the archive does not exist it is not
// created. Use create() for that.
// --------------------------------------------------------------------------------
function PclZip($p_zipname)
function __construct($p_zipname) // renamed to __construct()
{
// ----- Tests the zlib
if (!function_exists('gzopen'))
{
@@ -4210,12 +4209,21 @@
if ($p_entry['compression'] == 0) {
// ----- Reading the file
$p_string = @fread($this->zip_fd, $p_entry['compressed_size']);
try
{
$p_string = @fread($this->zip_fd, $p_entry['compressed_size']);
}
catch( \ValueError $e ) {}
}
else {
// ----- Reading the file
$v_data = @fread($this->zip_fd, $p_entry['compressed_size']);
try
{
$v_data = @fread($this->zip_fd, $p_entry['compressed_size']);
}
catch( \ValueError $e ) {}
// ----- Decompress the file
if (($p_string = @gzinflate($v_data)) === FALSE) {
+2 -2
View File
@@ -56,12 +56,12 @@ abstract class _Controller
* @brief Name of the method we called
*/
public $methodCalled = NULL;
/**
* Constructor
*
* @param \IPS\Api\Key $apiKey The API key being used to access, if applicable
* @param string $accessToken Access token for OAuth-authenticated requests
* @param array|null $accessToken Access token for OAuth-authenticated requests
* @return void
*/
public function __construct( \IPS\Api\Key $apiKey = NULL, $accessToken = NULL )
+80
View File
@@ -0,0 +1,80 @@
<?php
/**
* @brief Documentation Helper
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @since 11 Nov 2021
*/
namespace IPS\Api;
/* 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;
}
/**
* Documentation Helper
*/
abstract class _DocumentationHelper
{
public static function getDescriptionForClass( $class )
{
if ( method_exists( $class, 'apiOutput') )
{
$reflection = new \ReflectionMethod( $class, 'apiOutput' );
$decoded = \IPS\Api\Controller::decodeDocblock( $reflection->getDocComment() );
return \IPS\Theme::i()->getTemplate('api','core')->referenceTable( $decoded['details']['apiresponse'] );
}
return \IPS\Theme::i()->getTemplate( 'global' )->block( '', \IPS\Member::loggedIn()->language()->addToStack( 'class_no_apioutput_method' ) );
}
/**
* Get any additional classes referenced in the return types of this class
*
* @param string $class The classname
* @param bool $exclude If FALSE, will include this class itself in the return array
* @return array
*/
public static function getAdditionalClasses( $class, $exclude=FALSE )
{
if( !class_exists( $class ) or !method_exists( $class, 'apiOutput'))
{
return [];
}
$return = $exclude ? array() : array( $class => $class );
$reflection = new \ReflectionMethod( $class, 'apiOutput' );
$decoded = \IPS\Api\Controller::decodeDocblock( $reflection->getDocComment() );
foreach ( $decoded['details']['apiresponse'] as $response )
{
if ( mb_strpos( $response[0], '|' ) === FALSE AND !\in_array( $response[0], array( 'int', 'string', 'float', 'datetime', 'bool', 'object', 'array' ) ) )
{
if ( mb_substr( $response[0], 0, 1 ) == '[' )
{
if ( !\in_array( mb_substr( $response[0], 1, -1 ), $return ) and !\in_array( mb_substr( $response[0], 1, -1 ), array( 'int', 'string', 'float', 'datetime', 'bool', 'object', 'array' ) ) )
{
if( $returned = static::getAdditionalClasses( mb_substr( $response[0], 1, -1 ) ) )
{
$return = array_merge( $return, $returned );
}
}
}
elseif ( !\in_array( $response[0], $return ) )
{
if( $returned = static::getAdditionalClasses( $response[0] ) )
{
$return = array_merge( $return, $returned );
}
}
}
}
return $return;
}
}
+147 -212
View File
@@ -120,168 +120,163 @@ class _OAuthClient extends \IPS\Node\Model
$form->addTab('oauth_basic_settings');
$form->addHeader('oauth_basic_settings');
$form->add( new \IPS\Helpers\Form\Translatable( 'oauth_client_name', NULL, TRUE, array( 'app' => 'core', 'key' => ( $this->client_id ? "core_oauth_client_{$this->client_id}" : NULL ) ) ) );
if ( $type !== 'mobile' )
{
$form->add( new \IPS\Helpers\Form\Radio( 'oauth_client_type', $type, TRUE, array(
'options' => array(
'invision' => 'client_type_invision',
'wordpress' => 'client_type_wordpress',
'confidential' => 'client_type_confidential',
'public' => 'client_type_public',
),
'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' ),
)
) ) );
$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',
'password' => 'invision_grant_type_server_password',
),
), NULL, NULL, NULL, 'oauth_grant_types_invision' ) );
$confidentialGrant = new \IPS\Helpers\Form\CheckboxSet( 'oauth_grant_types_confidential', $this->client_id ? explode( ',', $this->grant_types ) : array( 'authorization_code' ), NULL, array(
'options' => array(
'authorization_code' => 'grant_type_authorization_code',
'implicit' => 'grant_type_implicit',
'password' => 'grant_type_password',
'client_credentials' => 'grant_type_client_credentials'
),
'toggles' => array(
'authorization_code' => array( 'oauth_pkce', 'oauth_use_refresh_tokens' )
)
), function( $val ) {
if ( !$val and \IPS\Request::i()->oauth_client_type === 'confidential' ) {
throw new \DomainException('form_required');
}
}, NULL, NULL, 'oauth_grant_types_confidential' );
$confidentialGrant->label = \IPS\Member::loggedIn()->language()->addToStack('oauth_grant_types');
$form->add( $confidentialGrant );
$publicGrant = new \IPS\Helpers\Form\CheckboxSet( 'oauth_grant_types_public', $this->client_id ? explode( ',', $this->grant_types ) : array( 'implicit' ), NULL, array(
'options' => array(
'authorization_code' => 'grant_type_authorization_code',
'implicit' => 'grant_type_implicit',
'password' => 'grant_type_password',
),
'toggles' => array(
'authorization_code' => array( 'oauth_pkce', 'oauth_use_refresh_tokens' )
)
), function( $val ) {
if ( !$val and \IPS\Request::i()->oauth_client_type === 'public' ) {
throw new \DomainException('form_required');
}
}, NULL, NULL, 'oauth_grant_types_public' );
$publicGrant->label = \IPS\Member::loggedIn()->language()->addToStack('oauth_grant_types');
$form->add( $publicGrant );
$redirectUris = json_decode( $this->redirect_uris, TRUE );
$form->add( new \IPS\Helpers\Form\Url( 'oauth_invision_endpoint', isset( $redirectUris[0] ) ? preg_replace( '#/oauth/callback/$#i', '/', $redirectUris[0] ) : NULL, NULL, array( 'placeholder' => 'https://othercommunity.example.com/', 'allowedProtocols' => NULL ), function( $val ) {
if ( !$val and \IPS\Request::i()->oauth_client_type == 'invision' ) {
$form->add( new \IPS\Helpers\Form\Radio( 'oauth_client_type', $type, TRUE, array(
'options' => array(
'invision' => 'client_type_invision',
'wordpress' => 'client_type_wordpress',
'confidential' => 'client_type_confidential',
'public' => 'client_type_public',
),
'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' ),
)
) ) );
$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',
'password' => 'invision_grant_type_server_password',
),
), NULL, NULL, NULL, 'oauth_grant_types_invision' ) );
$confidentialGrant = new \IPS\Helpers\Form\CheckboxSet( 'oauth_grant_types_confidential', $this->client_id ? explode( ',', $this->grant_types ) : array( 'authorization_code' ), NULL, array(
'options' => array(
'authorization_code' => 'grant_type_authorization_code',
'implicit' => 'grant_type_implicit',
'password' => 'grant_type_password',
'client_credentials' => 'grant_type_client_credentials'
),
'toggles' => array(
'authorization_code' => array( 'oauth_pkce', 'oauth_use_refresh_tokens' )
)
), function( $val ) {
if ( !$val and \IPS\Request::i()->oauth_client_type === 'confidential' ) {
throw new \DomainException('form_required');
}
}, NULL, NULL, 'oauth_grant_types_confidential' );
$confidentialGrant->label = \IPS\Member::loggedIn()->language()->addToStack('oauth_grant_types');
$form->add( $confidentialGrant );
$publicGrant = new \IPS\Helpers\Form\CheckboxSet( 'oauth_grant_types_public', $this->client_id ? explode( ',', $this->grant_types ) : array( 'implicit' ), NULL, array(
'options' => array(
'authorization_code' => 'grant_type_authorization_code',
'implicit' => 'grant_type_implicit',
'password' => 'grant_type_password',
),
'toggles' => array(
'authorization_code' => array( 'oauth_pkce', 'oauth_use_refresh_tokens' )
)
), function( $val ) {
if ( !$val and \IPS\Request::i()->oauth_client_type === 'public' ) {
throw new \DomainException('form_required');
}
}, NULL, NULL, 'oauth_grant_types_public' );
$publicGrant->label = \IPS\Member::loggedIn()->language()->addToStack('oauth_grant_types');
$form->add( $publicGrant );
$redirectUris = json_decode( $this->redirect_uris, TRUE );
$form->add( new \IPS\Helpers\Form\Url( 'oauth_invision_endpoint', isset( $redirectUris[0] ) ? preg_replace( '#/oauth/callback/$#i', '/', $redirectUris[0] ) : NULL, NULL, array( 'placeholder' => 'https://othercommunity.example.com/', 'allowedProtocols' => NULL ), function( $val ) {
if ( !$val and \IPS\Request::i()->oauth_client_type == 'invision' ) {
throw new \DomainException('form_required');
}
if ( $val and $val instanceof \IPS\Http\Url and $val->data[ \IPS\Http\Url::COMPONENT_FRAGMENT ] ) {
throw new \DomainException('oauth_redirect_uris_no_fragment');
}
if ( $val and rtrim( (string) $val, '/' ) === rtrim( \IPS\Settings::i()->base_url, '/' ) ) {
throw new \DomainException('oauth_invision_endpoint_internal');
}
}, NULL, NULL, 'oauth_invision_endpoint' ) );
$form->add( new \IPS\Helpers\Form\Url( 'oauth_wordpress_endpoint', isset( $redirectUris[0] ) ? $redirectUris[0] : NULL, NULL, array( 'placeholder' => 'https://wordpress.example.com/', 'allowedProtocols' => NULL ), function( $val ) {
if ( !$val and \IPS\Request::i()->oauth_client_type == 'wordpress' ) {
throw new \DomainException('form_required');
}
if ( $val and $val instanceof \IPS\Http\Url and $val->data[ \IPS\Http\Url::COMPONENT_FRAGMENT ] ) {
throw new \DomainException('oauth_redirect_uris_no_fragment');
}
}, NULL, NULL, 'oauth_wordpress_endpoint' ) );
$form->add( new \IPS\Helpers\Form\Radio( 'oauth_pkce', $this->pkce ?: 'none', FALSE, array( 'options' => array( 'S256' => 'oauth_pkce_256', 'plain' => 'oauth_pkce_plain', 'none' => 'oauth_pkce_none' ) ), NULL, NULL, NULL, 'oauth_pkce' ) );
$form->add( new \IPS\Helpers\Form\Stack( 'oauth_redirect_uris', $redirectUris, NULL, array( 'stackFieldType' => 'Url', 'placeholder' => 'https://www.example.com/redirect_uri', 'allowedProtocols' => NULL ), function( $val ) {
if ( !\in_array( \IPS\Request::i()->oauth_client_type, array('invision', 'wordpress') ) ) {
$chosenGrantTypes = \IPS\Request::i()->oauth_client_type === 'public' ? \IPS\Request::i()->oauth_grant_types_public : \IPS\Request::i()->oauth_grant_types_confidential;
if ( !$val and ( isset( $chosenGrantTypes['authorization_code'] ) or isset( $chosenGrantTypes['implicit'] ) ) ) {
throw new \DomainException('form_required');
}
if ( $val and $val instanceof \IPS\Http\Url and $val->data[ \IPS\Http\Url::COMPONENT_FRAGMENT ] ) {
throw new \DomainException('oauth_redirect_uris_no_fragment');
}
if ( $val and rtrim( (string) $val, '/' ) === rtrim( \IPS\Settings::i()->base_url, '/' ) ) {
throw new \DomainException('oauth_invision_endpoint_internal');
}
}, NULL, NULL, 'oauth_invision_endpoint' ) );
$form->add( new \IPS\Helpers\Form\Url( 'oauth_wordpress_endpoint', isset( $redirectUris[0] ) ? $redirectUris[0] : NULL, NULL, array( 'placeholder' => 'https://wordpress.example.com/', 'allowedProtocols' => NULL ), function( $val ) {
if ( !$val and \IPS\Request::i()->oauth_client_type == 'wordpress' ) {
throw new \DomainException('form_required');
}
if ( $val and $val instanceof \IPS\Http\Url and $val->data[ \IPS\Http\Url::COMPONENT_FRAGMENT ] ) {
throw new \DomainException('oauth_redirect_uris_no_fragment');
}
}, NULL, NULL, 'oauth_wordpress_endpoint' ) );
$form->add( new \IPS\Helpers\Form\Radio( 'oauth_pkce', $this->pkce ?: 'none', FALSE, array( 'options' => array( 'S256' => 'oauth_pkce_256', 'plain' => 'oauth_pkce_plain', 'none' => 'oauth_pkce_none' ) ), NULL, NULL, NULL, 'oauth_pkce' ) );
$form->add( new \IPS\Helpers\Form\Stack( 'oauth_redirect_uris', $redirectUris, NULL, array( 'stackFieldType' => 'Url', 'placeholder' => 'https://www.example.com/redirect_uri', 'allowedProtocols' => NULL ), function( $val ) {
if ( !\in_array( \IPS\Request::i()->oauth_client_type, array('invision', 'wordpress') ) ) {
$chosenGrantTypes = \IPS\Request::i()->oauth_client_type === 'public' ? \IPS\Request::i()->oauth_grant_types_public : \IPS\Request::i()->oauth_grant_types_confidential;
if ( !$val and ( isset( $chosenGrantTypes['authorization_code'] ) or isset( $chosenGrantTypes['implicit'] ) ) ) {
throw new \DomainException('form_required');
}
if ( $val and $val instanceof \IPS\Http\Url and $val->data[ \IPS\Http\Url::COMPONENT_FRAGMENT ] ) {
throw new \DomainException('oauth_redirect_uris_no_fragment');
}
}
}, NULL, NULL, 'oauth_redirect_uris' ) );
}
}
}, NULL, NULL, 'oauth_redirect_uris' ) );
$form->addHeader('oauth_authorization_screen');
$form->add( new \IPS\Helpers\Form\Radio( 'oauth_prompt', $this->prompt, FALSE, array( 'options' => array( 'none' => 'oauth_prompt_none', 'automatic' => 'oauth_prompt_automatic', 'reauthorize' => 'oauth_prompt_reauthorize', 'login' => 'oauth_prompt_login' ) ) ) );
if ( $type !== 'mobile' )
{
$form->add( new \IPS\Helpers\Form\YesNo( 'oauth_choose_scopes', $this->choose_scopes, FALSE, array(), NULL, NULL, NULL, 'oauth_choose_scopes' ) );
}
$form->add( new \IPS\Helpers\Form\YesNo( 'oauth_choose_scopes', $this->choose_scopes, FALSE, array(), NULL, NULL, NULL, 'oauth_choose_scopes' ) );
$form->add( new \IPS\Helpers\Form\YesNo( 'oauth_ucp', $this->ucp, FALSE ) );
if ( $type !== 'mobile' )
$form->addHeader('oauth_access_tokens');
$form->add( new \IPS\Helpers\Form\Interval( 'oauth_access_token_length', $this->access_token_length, NULL, array( 'valueAs' => \IPS\Helpers\Form\Interval::HOURS, 'unlimited' => 0, 'unlimitedLang' => 'forever' ), NULL, NULL, NULL, 'oauth_access_token_length' ) );
$form->add( new \IPS\Helpers\Form\YesNo( 'oauth_use_refresh_tokens', $this->use_refresh_tokens, NULL, array( 'togglesOn' => array( 'oauth_refresh_token_length' ) ), NULL, NULL, NULL, 'oauth_use_refresh_tokens' ) );
$form->add( new \IPS\Helpers\Form\Interval( 'oauth_refresh_token_length', $this->refresh_token_length, NULL, array( 'valueAs' => \IPS\Helpers\Form\Interval::DAYS, 'unlimited' => 0, 'unlimitedLang' => 'forever' ), NULL, NULL, NULL, 'oauth_refresh_token_length' ) );
$form->addTab('oauth_scopes');
$form->addMessage('oauth_scopes_blurb');
$matrix = new \IPS\Helpers\Form\Matrix;
$matrix->classes[] = 'cApiPermissionsMatrix';
$matrix->langPrefix = 'oauth_scope_';
$matrix->columns = array(
'name' => function( $key, $value, $data )
{
return new \IPS\Helpers\Form\Custom( $key, $value, FALSE, array(
'getHtml' => function( $field )
{
return \IPS\Theme::i()->getTemplate( 'api' )->oauthScopeField( $field->name, $field->value );
}
) );
},
'endpoints' => function( $key, $value, $data )
{
return new \IPS\Helpers\Form\Custom( $key, $value ?: array(), FALSE, array(
'getHtml' => function( $field )
{
$endpoints = \IPS\Api\Controller::getAllEndpoints();
foreach ( $endpoints as $key => $endpoint )
{
$pieces = explode('/', $key);
$endpointTree[ $pieces[0] ][ $pieces[1] ][ $key ] = $endpoint;
}
return \IPS\Theme::i()->getTemplate( 'api' )->permissionsFieldHtml( $endpointTree, $field->name, $field->value );
}
) );
}
);
if ( !$this->client_id )
{
$form->addHeader('oauth_access_tokens');
$form->add( new \IPS\Helpers\Form\Interval( 'oauth_access_token_length', $this->access_token_length, NULL, array( 'valueAs' => \IPS\Helpers\Form\Interval::HOURS, 'unlimited' => 0, 'unlimitedLang' => 'forever' ), NULL, NULL, NULL, 'oauth_access_token_length' ) );
$form->add( new \IPS\Helpers\Form\YesNo( 'oauth_use_refresh_tokens', $this->use_refresh_tokens, NULL, array( 'togglesOn' => array( 'oauth_refresh_token_length' ) ), NULL, NULL, NULL, 'oauth_use_refresh_tokens' ) );
$form->add( new \IPS\Helpers\Form\Interval( 'oauth_refresh_token_length', $this->refresh_token_length, NULL, array( 'valueAs' => \IPS\Helpers\Form\Interval::DAYS, 'unlimited' => 0, 'unlimitedLang' => 'forever' ), NULL, NULL, NULL, 'oauth_refresh_token_length' ) );
$form->addTab('oauth_scopes');
$form->addMessage('oauth_scopes_blurb');
$matrix = new \IPS\Helpers\Form\Matrix;
$matrix->classes[] = 'cApiPermissionsMatrix';
$matrix->langPrefix = 'oauth_scope_';
$matrix->columns = array(
'name' => function( $key, $value, $data )
{
return new \IPS\Helpers\Form\Custom( $key, $value, FALSE, array(
'getHtml' => function( $field )
{
return \IPS\Theme::i()->getTemplate( 'api' )->oauthScopeField( $field->name, $field->value );
}
) );
},
'endpoints' => function( $key, $value, $data )
{
return new \IPS\Helpers\Form\Custom( $key, $value ?: array(), FALSE, array(
'getHtml' => function( $field )
{
$endpoints = \IPS\Api\Controller::getAllEndpoints();
foreach ( $endpoints as $key => $endpoint )
{
$pieces = explode('/', $key);
$endpointTree[ $pieces[0] ][ $pieces[1] ][ $key ] = $endpoint;
}
return \IPS\Theme::i()->getTemplate( 'api' )->permissionsFieldHtml( $endpointTree, $field->name, $field->value );
}
) );
}
$matrix->rows[] = array(
'name' => array( 'key' => 'profile', 'desc' => \IPS\Member::loggedIn()->language()->get('oauth_default_scope_profile') ),
'endpoints' => array(
'core/me/GETindex' => array( 'access' => TRUE, 'log' => FALSE ),
)
);
$matrix->rows[] = array(
'name' => array( 'key' => 'email', 'desc' => \IPS\Member::loggedIn()->language()->get('oauth_default_scope_email') ),
'endpoints' => array(
'core/me/GETitem' => array( 'access' => TRUE, 'log' => FALSE ),
)
);
if ( !$this->client_id )
{
$matrix->rows[] = array(
'name' => array( 'key' => 'profile', 'desc' => \IPS\Member::loggedIn()->language()->get('oauth_default_scope_profile') ),
'endpoints' => array(
'core/me/GETindex' => array( 'access' => TRUE, 'log' => FALSE ),
)
);
$matrix->rows[] = array(
'name' => array( 'key' => 'email', 'desc' => \IPS\Member::loggedIn()->language()->get('oauth_default_scope_email') ),
'endpoints' => array(
'core/me/GETitem' => array( 'access' => TRUE, 'log' => FALSE ),
)
);
}
elseif ( $this->scopes and $scopes = json_decode( $this->scopes, TRUE ) )
{
foreach ( $scopes as $key => $data )
{
$matrix->rows[] = array(
'name' => array( 'key' => $key, 'desc' => $data['description'] ),
'endpoints' => $data['endpoints']
);
}
}
$form->addMatrix( 'scopes', $matrix );
}
elseif ( $this->scopes and $scopes = json_decode( $this->scopes, TRUE ) )
{
foreach ( $scopes as $key => $data )
{
$matrix->rows[] = array(
'name' => array( 'key' => $key, 'desc' => $data['description'] ),
'endpoints' => $data['endpoints']
);
}
}
$form->addMatrix( 'scopes', $matrix );
}
/**
@@ -299,20 +294,8 @@ class _OAuthClient extends \IPS\Node\Model
{
/* Normalise the settings */
$originalClientType = isset( $values['oauth_client_type'] ) ? $values['oauth_client_type'] : $this->type;
if ( $this->type === 'mobile' )
{
$values['oauth_client_type'] = 'mobile';
foreach ( static::mobileAppValues() as $k => $v )
{
$values["oauth_{$k}"] = $v;
}
$values['oauth_grant_types_public'] = $values['oauth_grant_types'];
$values['scopes'] = $values['oauth_scopes'];
$values['oauth_pkce'] = 'S256';
}
elseif ( $values['oauth_client_type'] === 'invision' )
if ( $values['oauth_client_type'] === 'invision' )
{
$values['oauth_client_type'] = 'confidential';
$values['oauth_grant_types_confidential'] = array( $values['oauth_invision_grant_type'] );
@@ -450,27 +433,6 @@ class _OAuthClient extends \IPS\Node\Model
return FALSE;
}
/**
* [Node] Does the currently logged in user have permission to delete this node?
*
* @return bool
*/
public function canDelete()
{
return $this->type !== 'mobile'; // Admin can deregister community which will in turn delete the client
}
/**
* [Node] Get whether or not this node is locked to current enabled/disabled status
*
* @note Return value NULL indicates the node cannot be enabled/disabled
* @return bool|null
*/
protected function get__locked()
{
return $this->type === 'mobile';
}
/**
* [Node] Get buttons to display in tree
* Example code explains return value
@@ -780,31 +742,4 @@ class _OAuthClient extends \IPS\Node\Model
return parent::delete();
}
/**
* Get special values for mobile app
*
* @return void
*/
final public static function mobileAppValues()
{
$redirect_uris = [ \IPS\APP_MULTICOMMUNITY_SCHEMA . '://auth' ];
if ( \IPS\Settings::i()->mobile_app_redirect_scheme )
{
$redirect_uris[] = \IPS\Settings::i()->mobile_app_redirect_scheme . '://auth';
}
return array(
'grant_types' => array('authorization_code'),
'redirect_uris' => $redirect_uris,
'access_token_length' => 168,
'use_refresh_tokens' => TRUE,
'refresh_token_length' => NULL,
'scopes' => array(),
'choose_scopes' => FALSE,
'type' => 'mobile',
'graphql' => TRUE
);
}
}
+1 -1
View File
@@ -83,7 +83,7 @@ class _PaginatedResponse extends Response
/**
* Data to output
*
* @return string
* @return array
*/
public function getOutput()
{
+171 -24
View File
@@ -20,7 +20,7 @@ if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
/**
* Webhook
*/
class _Webhook extends \IPS\Patterns\ActiveRecord
class _Webhook extends \IPS\Node\Model
{
/**
* @brief [ActiveRecord] Multiton Store
@@ -42,9 +42,103 @@ class _Webhook extends \IPS\Patterns\ActiveRecord
* @note Defined cache keys will be cleared automatically as needed
*/
protected $caches = array('webhooks');
/**
* Fire all webhooks for given event
* @brief [Node] Node Title
*/
public static $nodeTitle = 'api_webhooks';
/**
* @brief [Node] ACP Restrictions
* @code
array(
'app' => 'core', // The application key which holds the restrictrions
'module' => 'foo', // The module key which holds the restrictions
'map' => array( // [Optional] The key for each restriction - can alternatively use "prefix"
'add' => 'foo_add',
'edit' => 'foo_edit',
'permissions' => 'foo_perms',
'delete' => 'foo_delete'
),
'all' => 'foo_manage', // [Optional] The key to use for any restriction not provided in the map (only needed if not providing all 4)
'prefix' => 'foo_', // [Optional] Rather than specifying each key in the map, you can specify a prefix, and it will automatically look for restrictions with the key "[prefix]_add/edit/permissions/delete"
* @endcode
*/
protected static $restrictions = array(
'app' => 'core',
'module' => 'applications',
'prefix' => 'api_',
);
/**
* Set Default Values
*
* @return void
*/
public function setDefaultValues()
{
$this->_data['filters'] = '{}';
$this->_data['url'] = '';
$this->_data['events'] = '';
}
/**
* [Node] Add/Edit Form
*
* @param \IPS\Helpers\Form $form The form
* @return void
*/
public function form( &$form )
{
$form->add( new \IPS\Helpers\Form\Url( 'url', $this->url ?? "", TRUE ) );
$events = [];
foreach ( \IPS\Api\Webhook::getAvailableWebhooks() as $app => $hooks )
{
if( \count( $hooks ))
{
foreach( $hooks as $hook => $params)
{
$events[$hook] = "webhook_". $hook ;
}
}
}
$form->add( new \IPS\core\Form\WebhookSelector('webhook_events', $this->events ?? [], TRUE , [ 'options' => $events] ) );
}
/**
* [Node] Format form values from add/edit form for save
*
* @param array $values Values from the form
* @return array
*/
public function formatFormValues( $values )
{
$values['events'] = $values['webhook_events'];
unset( $values['webhook_events'] );
return $values;
}
public function get__title()
{
return (string) $this->url;
}
/**
* [Node] Get Node Description
*
* @return string|null
*/
protected function get__description()
{
return \IPS\Theme::i()->getTemplate( 'api', 'core' )->webhookDesc( $this );
}
/**
* This method will log the webhook to be fired, it's going to be fired via a task!
*
* @param string $event The event key
* @param mixed $data Data
@@ -52,14 +146,7 @@ class _Webhook extends \IPS\Patterns\ActiveRecord
* @return void
*/
public static function fire( $event, $data = NULL, $filters = array() )
{
/* Normalise data */
if ( \is_object( $data ) and method_exists( $data, 'apiOutput' ) )
{
$data = $data->apiOutput();
}
$data = $data ? json_encode( $data ) : NULL;
{
/* Get our webhooks from cache */
if ( !isset( \IPS\Data\Store::i()->webhooks ) )
{
@@ -87,6 +174,28 @@ class _Webhook extends \IPS\Patterns\ActiveRecord
$enable = FALSE;
if ( isset( \IPS\Data\Store::i()->webhooks[ $event ] ) )
{
/* Normalise data */
if ( \is_object( $data ) and method_exists( $data, 'apiOutput' ) )
{
$data = $data->apiOutput();
}
else if ( \is_array( $data ) )
{
foreach ( $data as $key => &$item )
{
/* Normalise data */
if ( \is_object( $item ) and method_exists( $item, 'apiOutput' ) )
{
$item = $item->apiOutput();
}
}
}
/* We need to replace and langstring hashes ( like node names) */
\IPS\Member::loggedIn()->language()->parseOutputForDisplay( $data );
$data = $data ? json_encode( $data ) : NULL;
/* Loop through each one... */
foreach ( \IPS\Data\Store::i()->webhooks[ $event ] as $id => $webhook )
{
@@ -168,31 +277,30 @@ class _Webhook extends \IPS\Patterns\ActiveRecord
/**
* Set Events
*
* @param array $events List of events
* @param string|array $events List of events
* @return void
*/
public function set_events( array $events )
public function set_events( $events )
{
if( !\is_array( $events ) )
{
$events = [$events];
}
$this->_data['events'] = implode( ',', $events );
}
/**
* Get URL
*
* @return array
* @return
*/
public function get_url()
{
if ( $this->_url === NULL )
{
$this->_url = new \IPS\Http\Url( $this->_data['url'] );
}
return $this->_url;
return $this->_data['url'] ? new \IPS\Http\Url( $this->_data['url'] ) : '';
}
/**
* Set Events
* Set Url
*
* @param \IPS\Http\Url $url The URL
* @return void
@@ -227,12 +335,13 @@ class _Webhook extends \IPS\Patterns\ActiveRecord
/**
* Get output for API
*
* @param \IPS\Member|NULL $authorizedMember The member making the API request or NULL for API Key / client_credentials
* @return array
* @apiresponse int id ID number
* @apiresponse array events List of events to subscribe to
* @apiresponse array events List of events this hook is subscribed to
* @apiresponse string url URL to send webhook to
*/
public function apiOutput()
public function apiOutput( \IPS\Member $authorizedMember = NULL )
{
return array(
'id' => $this->id,
@@ -240,4 +349,42 @@ class _Webhook extends \IPS\Patterns\ActiveRecord
'url' => (string) $this->url
);
}
}
public static function getAvailableWebhooks()
{
$hooks = [];
foreach ( \IPS\Application::applications() as $app )
{
if( $app->enabled )
{
$hooks[$app->directory] = $app->getWebhooks();
}
}
return $hooks;
}
/**
* Search
*
* @param string $column Column to search
* @param string $query Search query
* @param string|null $order Column to order by
* @param mixed $where Where clause
* @return array
*/
public static function search( $column, $query, $order=NULL, $where=array() )
{
if ( $column === '_title' )
{
$column = 'url';
}
if( $order == '_title' )
{
$order = 'url';
}
return parent::search( $column, $query, $order, $where );
}
}
+97 -33
View File
@@ -443,9 +443,8 @@ class _Application extends \IPS\Node\Model
{
$update = array();
if( $this->update_version )
if( ( $versions = json_decode( $this->update_version, TRUE ) ) AND is_iterable( $versions ) )
{
$versions = json_decode( $this->update_version, TRUE );
if ( \is_array( $versions ) and !isset( $versions[0] ) and isset( $versions['longversion'] ) )
{
$versions = array( $versions );
@@ -810,7 +809,7 @@ EOF;
{
$classes[ $name ] = new $classname( $checkAccess === TRUE ? \IPS\Member::loggedIn() : ( $checkAccess === FALSE ? NULL : $checkAccess ) );
}
catch( \RuntimeException $e ){}
catch( \RuntimeException | \OutOfRangeException $e ){}
}
}
}
@@ -1019,7 +1018,7 @@ EOF;
);
/* Upgrade */
if( !$this->protected AND !\IPS\DEMO_MODE AND $this->marketplace_id === NULL AND \IPS\IPS::canManageResources() )
if( !$this->protected AND !\IPS\DEMO_MODE AND $this->marketplace_id === NULL AND \IPS\IPS::canManageResources() AND \IPS\IPS::checkThirdParty() )
{
$buttons['upgrade'] = array(
'icon' => 'upload',
@@ -1084,6 +1083,15 @@ EOF;
'link' => \IPS\Http\Url::internal( "app=core&module=applications&controller=developer&appKey={$this->_id}" ),
);
}
if( !\IPS\IN_DEV ) {
$buttons['export'] = array(
'icon' => 'download',
'title' => 'download',
'link' => \IPS\Http\Url::internal("app=core&module=applications&controller=applications&appKey={$this->_id}&do=downloadMafia")
);
}
return $buttons;
}
@@ -1100,6 +1108,7 @@ EOF;
{
return TRUE;
}
return $this->enabled and ( !\in_array( $this->directory, \IPS\IPS::$ipsApps ) or $this->version == \IPS\Application::load('core')->version );
}
@@ -1174,7 +1183,7 @@ EOF;
*/
public function get__disabledMessage()
{
if ( $this->_locked and $this->directory != 'core' )
if ( $this->_locked and $this->directory != 'core' AND \in_array( $this->directory, \IPS\IPS::$ipsApps ) )
{
return \IPS\Member::loggedIn()->language()->addToStack('app_force_disabled');
}
@@ -1204,16 +1213,6 @@ EOF;
}
}
/**
* Return an array of specific links to add to the sitemap
*
* @return array
*/
public function sitemapLinks()
{
return array();
}
/**
* Get the authors website
*
@@ -1235,6 +1234,11 @@ EOF;
*/
public function get__badge()
{
if ( \IPS\CIC AND \IPS\IPS::isManaged() AND \in_array( $this->directory, \IPS\IPS::$ipsApps ) )
{
return NULL;
}
if ( $availableUpgrade = $this->availableUpgrade( TRUE ) )
{
return array(
@@ -2689,13 +2693,11 @@ EOF;
{
if( file_exists( $this->getApplicationPath() . "/data/widgets.json" ) )
{
$currentWidgets = iterator_to_array( \IPS\Db::i()->select( '`key`', 'core_widgets', array( 'app=?', $this->directory ) ) );
\IPS\Db::i()->delete( 'core_widgets', array( 'app=?', $this->directory ) );
$inserts = array();
foreach ( json_decode( file_get_contents( $this->getApplicationPath() . "/data/widgets.json" ), TRUE ) as $key => $json )
{
if ( ! \in_array( $key, $currentWidgets ) )
{
$inserts[] = array(
'app' => $this->directory,
'key' => $key,
@@ -2705,8 +2707,7 @@ EOF;
'allow_reuse' => ( isset( $json['allow_reuse'] ) ? $json['allow_reuse'] : 0 ),
'menu_style' => ( isset( $json['menu_style'] ) ? $json['menu_style'] : 'menu' ),
'embeddable' => ( isset( $json['embeddable'] ) ? $json['embeddable'] : 0 ),
);
}
);
}
if( \count( $inserts ) )
@@ -3472,7 +3473,11 @@ EOF;
{
if ( $data = @json_decode( \file_get_contents( \IPS\Application::load( $appKey )->getApplicationPath() . "/setup/upg_{$versionId}/whatsnew.json" ), TRUE ) )
{
\IPS\Db::i()->insert( 'core_members_features', $data );
try
{
\IPS\Db::i()->insert( 'core_members_features', $data );
}
catch ( \IPS\Db\Exception $e) {}
}
}
}
@@ -4144,6 +4149,12 @@ EOF;
/* Strip the params tag */
$html = str_replace( $params[0], '', $html );
/* Enforce \n line endings */
if( mb_strtolower( mb_substr( PHP_OS, 0, 3 ) ) === 'win' )
{
$html = str_replace( "\r\n", "\n", $html );
}
/* Figure out some details */
$extension = mb_substr( $file->getFilename(), mb_strrpos( $file->getFilename(), '.' ) + 1 );
@@ -4631,7 +4642,7 @@ EOF;
}
/* If we are in the AdminCP, and we have permission to manage applications, then we have access */
if( \IPS\Dispatcher::hasInstance() AND \IPS\Dispatcher::i()->controllerLocation === 'admin' AND \IPS\Member::loggedIn()->hasAcpRestriction( 'core', 'applications', 'app_manage' ) )
if( \IPS\Dispatcher::hasInstance() AND \IPS\Dispatcher::i()->controllerLocation === 'admin' AND ( !$memberOrGroup AND \IPS\Member::loggedIn()->hasAcpRestriction( 'core', 'applications', 'app_manage' ) ) )
{
return TRUE;
}
@@ -4780,12 +4791,18 @@ EOF;
\IPS\Db::i()->delete( 'core_sys_conf_settings', array( 'conf_app=?', $this->directory ) );
\IPS\Db::i()->delete( 'core_queue', array( 'app=?', $this->directory ) );
\IPS\Db::i()->delete( 'core_follow', array( 'follow_app=?', $this->directory ) );
\IPS\Db::i()->delete( 'core_follow_count_cache', array( "class LIKE CONCAT( ?, '%' )", "IPS\\\\{$this->directory}" ) );
\IPS\Db::i()->delete( 'core_view_updates', array( "classname LIKE CONCAT( ?, '%' )", "IPS\\\\{$this->directory}" ) );
\IPS\Db::i()->delete( 'core_moderator_logs', array( 'appcomponent=?', $this->directory ) );
\IPS\Db::i()->delete( 'core_member_history', array( 'log_app=?', $this->directory ) );
\IPS\Db::i()->delete( 'core_acp_notifications', array( 'app=?', $this->directory ) );
\IPS\Db::i()->delete( 'core_solved_index', array( 'app=?', $this->directory ) );
\IPS\Db::i()->delete( 'core_notifications', array( 'notification_app=?', $this->directory ) );
$rulesToDelete = iterator_to_array( \IPS\Db::i()->select( 'id', 'core_achievements_rules', [ "action LIKE CONCAT( ?, '_%' )", $this->directory ] ) );
\IPS\Db::i()->delete( 'core_achievements_rules', \IPS\Db::i()->in( 'id', $rulesToDelete ) );
\IPS\Db::i()->delete( 'core_achievements_log_milestones', \IPS\Db::i()->in( 'milestone_rule', $rulesToDelete ) );
foreach( $this->extensions( 'core', 'AdminNotifications', FALSE ) AS $adminNotificationExtension )
{
$exploded = explode( '\\', $adminNotificationExtension );
@@ -5139,15 +5156,8 @@ EOF;
\IPS\Settings::i()->clearCache();
/* Remove the files and folders, if possible (if not IN_DEV and not in DEMO_MODE) */
if ( \IPS\CIC2 )
{
if ( !\in_array( $this->directory, \IPS\IPS::$ipsApps ) )
{
\IPS\Cicloud\folderDelete( "applications/{$this->directory}" );
}
}
elseif ( !\IPS\IN_DEV AND !\IPS\DEMO_MODE AND file_exists( \IPS\ROOT_PATH . '/applications/' . $this->directory ) )
/* Remove the files and folders, if possible (if not IN_DEV and not in DEMO_MODE and not on platform) */
if ( !\IPS\CIC2 AND !\IPS\IN_DEV AND !\IPS\DEMO_MODE AND file_exists( \IPS\ROOT_PATH . '/applications/' . $this->directory ) )
{
try
{
@@ -5396,4 +5406,58 @@ EOF;
return \IPS\SITE_FILES_PATH;
}
}
}
/**
* Returns a list of all existing webhooks and their payload in this app.
*
* @return array
*/
public function getWebhooks() : array
{
// Fetch all the content classes
$classes = [];
$hooks = [];
foreach ( $this->extensions( 'core', 'ContentRouter' ) as $router )
{
foreach ( $router->classes as $class )
{
$classes[] = $class;
if ( isset( $class::$commentClass ) )
{
$commentClass = $class::$commentClass;
$classes[] = $commentClass;
}
if ( isset( $class::$reviewClass ) )
{
$reviewClass = $class::$reviewClass;
$classes[] = $reviewClass;
}
}
}
foreach( $classes as $class )
{
$key = str_replace( '\\', '', \substr( $class, 3 ) );
$hooks[$key .'_create'] = $class;
$hooks[$key .'_delete'] = $class;
\IPS\Member::loggedIn()->language()->words[ 'webhook_' . $key .'_create' ] = \IPS\Member::loggedIn()->language()->addToStack('webhook_contentitem_created', FALSE, ['sprintf' => [ $class::_indefiniteArticle() ]]);
\IPS\Member::loggedIn()->language()->words[ 'webhook_' . $key .'_delete' ] = \IPS\Member::loggedIn()->language()->addToStack('webhook_contentitem_deleted', FALSE, ['sprintf' => [ $class::_indefiniteArticle() ]]);
}
return $hooks;
}
/**
* Do Member Check
*
* @return \IPS\Http\Url|NULL
*/
public function doMemberCheck(): ?\IPS\Http\Url
{
return NULL;
}
}
+155 -3
View File
@@ -239,13 +239,13 @@ class _CommentController extends \IPS\Api\Controller
$class = $this->class;
if ( \in_array( 'IPS\Content\Review', class_parents( $class ) ) )
{
$comment = $class::create( $item, $content, FALSE, \intval( \IPS\Request::i()->rating ), $author->member_id ? NULL : $author->real_name, $author, $date, ( !$this->member and \IPS\Request::i()->ip_address ) ? \IPS\Request::i()->ip_address : \IPS\Request::i()->ipAddress(), $hidden );
$comment = $class::create( $item, $content, FALSE, \intval( \IPS\Request::i()->rating ), $author->member_id ? NULL : $author->real_name, $author, $date, ( !$this->member and \IPS\Request::i()->ip_address ) ? \IPS\Request::i()->ip_address : \IPS\Request::i()->ipAddress(), $hidden, ( isset( \IPS\Request::i()->anonymous ) ? (bool) \IPS\Request::i()->anonymous : NULL ) );
}
else
{
$comment = $class::create( $item, $content, FALSE, $author->member_id ? NULL : $author->real_name, NULL, $author, $date, ( !$this->member and \IPS\Request::i()->ip_address ) ? \IPS\Request::i()->ip_address : \IPS\Request::i()->ipAddress(), $hidden );
$comment = $class::create( $item, $content, FALSE, $author->member_id ? NULL : $author->real_name, NULL, $author, $date, ( !$this->member and \IPS\Request::i()->ip_address ) ? \IPS\Request::i()->ip_address : \IPS\Request::i()->ipAddress(), $hidden, ( isset( \IPS\Request::i()->anonymous ) ? (bool) \IPS\Request::i()->anonymous : NULL ) );
}
/* Index */
if ( $item instanceof \IPS\Content\Searchable )
{
@@ -369,6 +369,158 @@ class _CommentController extends \IPS\Api\Controller
/* Return */
return new \IPS\Api\Response( 200, $comment->apiOutput( $this->member ) );
}
/**
* Delete a reaction to a comment
*
* @param $id
* @return \IPS\Api\Response
* @throws \IPS\Api\Exception
*/
public function _reactRemove( $id ): \IPS\Api\Response
{
try
{
$member = \IPS\Member::load( \IPS\Request::i()->author );
}
catch( \OutOfRangeException $e )
{
throw new \IPS\Api\Exception( 'NO_AUTHOR', '1S425/6', 404 );
}
try
{
$class = $this->class;
if ( $this->member )
{
$object = $class::loadAndCheckPerms( $id, $this->member );
}
else
{
$object = $class::load( $id );
}
$object->removeReaction( $member );
return new \IPS\Api\Response( 200, $object->apiOutput( $this->member ) );
}
catch ( \DomainException $e )
{
throw new \IPS\Api\Exception( $e->getMessage(), '1S425/7', 403 );
}
catch ( \OutOfRangeException $e )
{
throw new \IPS\Api\Exception( 'INVALID_ID', '1S425/8', 404 );
}
}
/**
* React to a comment
*
* @param $id
* @return \IPS\Api\Response
* @throws \IPS\Api\Exception
*/
public function _reactAdd( $id ): \IPS\Api\Response
{
try
{
$reaction = \IPS\Content\Reaction::load( \IPS\Request::i()->id );
}
catch( \OutOfRangeException $e )
{
throw new \IPS\Api\Exception( 'NO_REACTION', '1S425/2', 404 );
}
try
{
$member = \IPS\Member::load( \IPS\Request::i()->author );
}
catch( \OutOfRangeException $e )
{
throw new \IPS\Api\Exception( 'NO_AUTHOR', '1S425/3', 404 );
}
try
{
$class = $this->class;
if ( $this->member )
{
$object = $class::loadAndCheckPerms( $id, $this->member );
}
else
{
$object = $class::load( $id );
}
$object->react( $reaction, $member );
return new \IPS\Api\Response( 200, $object->apiOutput( $this->member ) );
}
catch ( \DomainException $e )
{
throw new \IPS\Api\Exception( 'REACT_ERROR_' . $e->getMessage(), '1S425/4', 403 );
}
catch ( \OutOfRangeException $e )
{
throw new \IPS\Api\Exception( 'INVALID_ID', '1S425/5', 404 );
}
}
/**
* Report a comment
*
* @param $id
* @return \IPS\Api\Response
* @throws \IPS\Api\Exception
*/
public function _report( $id ): \IPS\Api\Response
{
try
{
$member = \IPS\Member::load( \IPS\Request::i()->author );
}
catch( \OutOfRangeException $e )
{
throw new \IPS\Api\Exception( 'NO_AUTHOR', '1S425/B', 404 );
}
$class = $this->class;
$idColumn = $class::$databaseColumnId;
if ( $this->member )
{
$object = $class::loadAndCheckPerms( $id, $this->member );
}
else
{
$object = $class::load( $id );
}
/* Has this member already reported this in the past 24 hours */
try
{
$index = \IPS\core\Reports\Report::loadByClassAndId( \get_class( $object ), $object->$idColumn );
$report = \IPS\Db::i()->select( '*', 'core_rc_reports', array( 'rid=? and report_by=? and date_reported > ?', $index->id, $member, time() - ( \IPS\Settings::i()->automoderation_report_again_mins * 60 ) ) )->first();
/* They have aleady reported, so do nothing */
throw new \IPS\Api\Exception( 'REPORTED_ALREADY', '1S425/C', 404 );
}
catch( \Exception $e )
{
/* No issues here */
}
try
{
$object->report( ( isset( \IPS\Request::i()->message ) ? \IPS\Request::i()->message : '' ), ( isset(\IPS\Request::i()->report_type) ? \IPS\Request::i()->report_type : 0 ), $member );
}
catch( \UnexpectedValueException $e )
{
throw new \IPS\Api\Exception( 'REPORT_ERROR_' . $e->getMessage(), '1S425/B', 403 );
}
return new \IPS\Api\Response( 200, $object->apiOutput( $this->member ) );
}
}
+30 -9
View File
@@ -339,6 +339,23 @@ class _ItemController extends \IPS\Api\Controller
}
}
/** We intentionally allow anonymous content in nodes where it's possible (but where it's disabled via the settings */
if( isset( \IPS\Request::i()->anonymous ) AND \in_array( 'IPS\Content\Anonymous', class_implements( \get_class( $item ) ) ) )
{
/* we need to save the item before we set the anonymous data */
$idColumn = $item::$databaseColumnId;
if ( !$item->$idColumn )
{
$item->save();
}
try
{
$item->setAnonymous( (bool) \IPS\Request::i()->anonymous, $item->author() );
}
catch ( \BadMethodCallException $e ){}
}
/* Update first comment if required, and it's not a new item */
$field = isset( $item::$databaseColumnMap['first_comment_id'] ) ? $item::$databaseColumnMap['first_comment_id'] : NULL;
$commentClass = $item::$commentClass;
@@ -392,9 +409,10 @@ class _ItemController extends \IPS\Api\Controller
/* Create item */
$item = $class::createItem( $author, ( !$this->member and \IPS\Request::i()->ip_address ) ? \IPS\Request::i()->ip_address : \IPS\Request::i()->ipAddress(), $date, $container );
$this->_createOrUpdate( $item, 'add' );
$item->save();
/* Create post */
if ( $class::$firstCommentRequired )
{
@@ -404,9 +422,9 @@ class _ItemController extends \IPS\Api\Controller
{
$postContents = \IPS\Text\Parser::parseStatic( $postContents, TRUE, NULL, $this->member, $class::$application . '_' . mb_ucfirst( $class::$module ) );
}
$commentClass = $item::$commentClass;
$post = $commentClass::create( $item, $postContents, TRUE, $author->member_id ? NULL : $author->real_name, NULL, $author, $date, ( !$this->member and \IPS\Request::i()->ip_address ) ? \IPS\Request::i()->ip_address : \IPS\Request::i()->ipAddress() );
$post = $commentClass::create( $item, $postContents, TRUE, $author->member_id ? NULL : $author->real_name, NULL, $author, $date, ( !$this->member and \IPS\Request::i()->ip_address ) ? \IPS\Request::i()->ip_address : \IPS\Request::i()->ipAddress(), NULL, ( isset( \IPS\Request::i()->anonymous ) ? (bool) \IPS\Request::i()->anonymous : NULL ) );
if ( isset( $class::$databaseColumnMap['first_comment_id'] ) )
{
@@ -516,21 +534,24 @@ class _ItemController extends \IPS\Api\Controller
}
$itemIdColumn = $itemClass::$databaseColumnId;
$where [] = array( $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['item'] . '=?', $item->$itemIdColumn );
/* Hidden? */
if ( isset( \IPS\Request::i()->hidden ) AND \in_array( 'IPS\Content\Hideable', class_implements( $commentClass ) ) )
/* Hideable? */
if ( \in_array( 'IPS\Content\Hideable', class_implements( $commentClass ) ) )
{
if ( \IPS\Request::i()->hidden )
/* If request for hidden comments, only show if request is via API key or by a moderator */
if ( isset( \IPS\Request::i()->hidden ) AND \IPS\Request::i()->hidden
AND ( $this->member === NULL OR $commentClass::modPermission( 'view_hidden', $this->member ) ) )
{
if ( isset( $commentClass::$databaseColumnMap['hidden'] ) )
{
$where[] = array( $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['hidden'] . '<>0' );
$where[] = array( $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['hidden'] . ' NOT IN( -3, -2 )' );
}
else
{
$where[] = array( $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['approved'] . '<>1' );
$where[] = array( $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['approved'] . ' NOT IN( -3, -2 )' );
}
}
/* Do not show hidden comments */
else
{
if ( isset( $commentClass::$databaseColumnMap['hidden'] ) )
+31 -6
View File
@@ -60,7 +60,7 @@ trait ClubContainer
*/
public function clubCheckRules()
{
if ( $club = $this->club() AND !$club->rulesAcknowledged() )
if ( $club = $this->club() AND !$club->rulesAcknowledged() AND !\IPS\Member::loggedIn()->modPermission( 'can_access_all_clubs' ) )
{
\IPS\Output::i()->redirect( $club->url()->setQueryString( 'do', 'rules' )->addRef( \IPS\Request::i()->url() ) );
}
@@ -94,7 +94,7 @@ trait ClubContainer
* @param \IPS\Helpers\Form $form Form object
* @return void
*/
public function clubForm( \IPS\Helpers\Form $form )
public function clubForm( \IPS\Helpers\Form $form, \IPS\Member\Club $club )
{
$itemClass = static::$contentItemClass;
$form->add( new \IPS\Helpers\Form\Text( 'club_node_name', $this->_id ? $this->_title : \IPS\Member::loggedIn()->language()->addToStack( static::clubFrontTitle() ), TRUE, array( 'maxLength' => 255 ) ) );
@@ -116,6 +116,7 @@ trait ClubContainer
$this->_saveClubForm( $club, $values );
$needToUpdatePermissions = FALSE;
if ( !$haveId )
{
$clubIdColumn = $this->clubIdColumn();
@@ -125,15 +126,21 @@ trait ClubContainer
'club_id' => $club->id,
'node_class' => $nodeClass,
'node_id' => $this->_id,
'name' => $values['club_node_name']
'name' => $values['club_node_name'],
'public' => ( isset( $values['club_node_public'] ) ) ? $values['club_node_public'] : 0
) );
$needToUpdatePermissions = TRUE;
}
else
{
if( isset( $values['club_node_public'] ) AND $values['club_node_public'] != $this->isPublic() )
{
$needToUpdatePermissions = TRUE;
}
$this->save();
\IPS\Db::i()->update( 'core_clubs_node_map', array( 'name' => $values['club_node_name'] ), 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'] ?? 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'] );
@@ -197,9 +204,10 @@ trait ClubContainer
case $club::TYPE_OPEN:
case $club::TYPE_READONLY:
$insert[ 'perm_' . $v ] = '*';
break;
break;
case $club::TYPE_CLOSED:
$insert[ 'perm_' . $v ] = ( $this->isPublic() ) ? "*" : "cm,c{$club->id}";
break;
case $club::TYPE_PRIVATE:
$insert[ 'perm_' . $v ] = "cm,c{$club->id}";
break;
@@ -398,4 +406,21 @@ trait ClubContainer
return parent::parents();
}
}
/**
* Is public
*
* @return Bool
*/
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();
}
catch ( \UnderflowException $e )
{
return FALSE;
}
}
}
+210 -20
View File
@@ -59,15 +59,18 @@ abstract class _Comment extends \IPS\Content
* @param \IPS\DateTime|NULL $time The time
* @param string|NULL $ipAddress The IP address or NULL to detect automatically
* @param int|NULL $hiddenStatus NULL to set automatically or override: 0 = unhidden; 1 = hidden, pending moderator approval; -1 = hidden (as if hidden by a moderator)
* @param int|NULL $anonymous NULL for no value, 0 or 1 for a value (0=no, 1=yes)
* @return static
*/
public static function create( $item, $comment, $first=FALSE, $guestName=NULL, $incrementPostCount=NULL, $member=NULL, \IPS\DateTime $time=NULL, $ipAddress=NULL, $hiddenStatus=NULL )
public static function create( $item, $comment, $first=FALSE, $guestName=NULL, $incrementPostCount=NULL, $member=NULL, \IPS\DateTime $time=NULL, $ipAddress=NULL, $hiddenStatus=NULL, $anonymous=NULL )
{
if ( $member === NULL )
{
$member = \IPS\Member::loggedIn();
}
$idColumn = $item::$databaseColumnId;
/* Create the object */
$obj = new static;
foreach ( array( 'item', 'date', 'author', 'author_name', 'content', 'ip_address', 'first', 'approved', 'hidden' ) as $k )
@@ -78,7 +81,6 @@ abstract class _Comment extends \IPS\Content
switch ( $k )
{
case 'item':
$idColumn = $item::$databaseColumnId;
$val = $item->$idColumn;
break;
@@ -184,42 +186,52 @@ abstract class _Comment extends \IPS\Content
}
/* Check if profanity filters should mod-queue this comment */
$obj->checkProfanityFilters( $first );
$obj->checkProfanityFilters( $first, FALSE );
/* Save the comment */
$obj->save();
/* Set anonymous status */
if( $anonymous !== NULL )
{
try
{
$obj->setAnonymous( (bool) $anonymous, $member );
}
catch ( \Exception $e ) { }
}
/* Increment post count */
try
{
if ( !$obj->isAnonymous() and !$obj->hidden() and ( $incrementPostCount === TRUE or ( $incrementPostCount === NULL and static::incrementPostCount( $item->container() ) ) ) )
{
$obj->author()->member_posts++;
$member->member_posts++;
}
}
catch( \BadMethodCallException $e ) { }
/* Update member's last post and daily post limits */
if( $obj->author()->member_id AND $obj::incrementPostCount() )
if( $member->member_id AND $obj::incrementPostCount() )
{
$obj->author()->member_last_post = time();
$member->member_last_post = time();
/* Update posts per day limits */
if ( $obj->author()->group['g_ppd_limit'] )
if ( $member->group['g_ppd_limit'] )
{
$current = $obj->author()->members_day_posts;
$current = $member->members_day_posts;
$current[0] += 1;
if ( $current[1] == 0 )
{
$current[1] = \IPS\DateTime::create()->getTimestamp();
}
$obj->author()->members_day_posts = $current;
$member->members_day_posts = $current;
}
$obj->author()->save();
$member->save();
}
/* Send notifications and dish out points */
@@ -228,7 +240,7 @@ abstract class _Comment extends \IPS\Content
if ( !$obj->hidden() and ( !$first or !$item::$firstCommentRequired ) )
{
$obj->sendNotifications();
$obj->author()->achievementAction( 'core', 'Comment', $obj );
$member->achievementAction( 'core', 'Comment', $obj );
}
else if( $obj->hidden() === 1 )
{
@@ -241,10 +253,45 @@ abstract class _Comment extends \IPS\Content
{
\IPS\Api\Webhook::fire( str_replace( '\\', '', \substr( \get_called_class(), 3 ) ) . '_create', $obj, $obj->webhookFilters() );
}
/* DataLayer Event */
if ( \IPS\Settings::i()->core_datalayer_enabled and !( $item::$firstCommentRequired and $obj->isFirst() ) )
{
if ( isset( $item::$title ) and $item::$title === \IPS\core\Statuses\Status::$title )
{
$event = 'social_reply';
$member = \IPS\Member::load( $item->member_id );
$properties = array(
'profile_id' => $member->member_id,
'profile_name' => $member->name,
);
}
else
{
$event = 'comment_create';
$properties = $obj->getDataLayerProperties();
}
if ( $properties['content_area'] === 'personal_messages' )
{
return;
}
$properties = array_merge( $properties, array(
'author_id' => $obj->author()->member_id,
'author_name' => $obj->author()->name ?? 'Guest',
) );
\IPS\core\DataLayer::i()->addEvent( $event, $properties );
}
/* Update item */
$obj->postCreate();
/* This needs to happen before the index is added */
$parameters = array_merge( array( 'reply-' . $item::$application . '/' . $item::$module . '-' . $item->$idColumn ), $obj->attachmentIds() );
\IPS\File::claimAttachments( ...$parameters );
/* Add to search index */
if ( $obj instanceof \IPS\Content\Searchable )
{
@@ -254,6 +301,32 @@ abstract class _Comment extends \IPS\Content
/* Return */
return $obj;
}
/**
* @brief Field cache for getDataLayerProperties
*/
protected $_dataLayerProperties = array();
/**
* Get the properties that can be added to the datalayer for this key
*
* @return array
*/
public function getDataLayerProperties()
{
if ( empty( $this->_dataLayerProperties ) )
{
$item = $this->item();
$this->_dataLayerProperties = $item->getDataLayerProperties( $this );
if ( $this->_dataLayerProperties['content_type'] === 'question' )
{
$this->_dataLayerProperties['comment_type'] = 'answer';
}
$this->_dataLayerProperties = \IPS\core\DataLayer::i()->filterProperties( $this->_dataLayerProperties );
}
return $this->_dataLayerProperties;
}
/**
* Join profile fields when loading comments?
@@ -376,6 +449,18 @@ abstract class _Comment extends \IPS\Content
{
$item->container()->rebuildPostedIn( array( $item->$itemIdColumn ), array( $this->author() ) );
}
/* Update trending */
$class = \get_class( $item );
if( \IPS\Application::appIsEnabled( 'cloud' ) and \IPS\cloud\Realtime::i()->isEnabled('trending') and $class::$includeInTrending )
{
/* Score by timestamp for trending */
try
{
\IPS\Redis::i()->zIncrBy( 'trending', time(), $class . '__' . $item->$itemIdColumn );
}
catch( \Exception $e ) { }
}
}
/**
@@ -827,6 +912,12 @@ abstract class _Comment extends \IPS\Content
{
$item->clearCachedStatistics();
}
/* Send the content items URL to IndexNow if the guest can view it */
if( $item->canView( new \IPS\Member ) )
{
\IPS\core\IndexNow::addUrlToQueue( $item->url() );
}
}
/**
@@ -844,6 +935,48 @@ abstract class _Comment extends \IPS\Content
$itemColumn = static::$databaseColumnMap['item'];
$commentIdColumn = static::$databaseColumnId;
$this->$itemColumn = $item->$idColumn;
/* Does this comment's hidden status align with its item's hidden status? */
if ( $this instanceof \IPS\Content\Hideable AND $this->hidden() !== 1 )
{
if ( $item->hidden() === -1 )
{
if ( isset( static::$databaseColumnMap['hidden'] ) )
{
$column = static::$databaseColumnMap['hidden'];
/* if this comment was directly hidden, we don't want to inherit its status from the item */
if ( \intval( $this->$column ) !== -1 )
{
$this->$column = 2;
}
}
elseif ( isset( static::$databaseColumnMap['approved'] ) )
{
$column = static::$databaseColumnMap['approved'];
$this->$column = -1;
}
}
elseif ( $this->hidden() === -1 )
{
if ( isset( static::$databaseColumnMap['hidden'] ) )
{
$column = static::$databaseColumnMap['hidden'];
/* if this comment was directly hidden, we don't want to inherit its status from the item */
if ( \intval( $this->$column ) !== -1 )
{
$this->$column = 0;
}
}
elseif ( isset( static::$databaseColumnMap['approved'] ) )
{
$column = static::$databaseColumnMap['approved'];
$this->$column = 1;
}
}
}
$this->save();
/* The new item needs to re-claim any attachments associated with this comment */
@@ -865,7 +998,16 @@ abstract class _Comment extends \IPS\Content
}
/* Update member map */
\IPS\Db::i()->update( 'core_item_member_map', array( 'map_item_id' => $item->$idColumn ), array( 'map_class=? and map_item_id=?', \get_class( $oldItem ), $oldItem->$idColumn ) );
try
{
\IPS\Db::i()->update( 'core_item_member_map', array( 'map_item_id' => $item->$idColumn ), array( 'map_class=? and map_item_id=?', \get_class( $oldItem ), $oldItem->$idColumn ) );
}
catch( \IPS\Db\Exception $e )
{
/* If we are splitting to a topic that this member has already posted in, this will cause a duplicate key issue. Best to remove rows and let the next page load rebuild */
\IPS\Db::i()->delete( 'core_item_member_map', array( 'map_class=? and map_item_id=?', \get_class( $oldItem ), $oldItem->$idColumn ) );
\IPS\Db::i()->delete( 'core_item_member_map', array( 'map_class=? and map_item_id=?', \get_class( $item ), $item->$idColumn ) );
}
if( $skip === FALSE )
{
@@ -883,6 +1025,23 @@ abstract class _Comment extends \IPS\Content
$item->clearCachedStatistics();
}
}
/* Send the content items URL to IndexNow */
$noIndexUrls = [];
$guest = new \IPS\Member;
if( $oldItem->canView( $guest ) )
{
$noIndexUrls[] = $oldItem->url();
}
if( $item->canView( $guest ) )
{
$noIndexUrls[] = $item->url();
}
if( $oldItem->canView( new \IPS\Member ) )
{
\IPS\core\IndexNow::addUrlsToQueue( $noIndexUrls );
}
}
/**
@@ -1505,7 +1664,7 @@ abstract class _Comment extends \IPS\Content
$attachments[] = array( 'core_Clubs' => $club->cover_photo );
}
return \count( $attachments ) ? $attachments : NULL;
return \count( $attachments ) ? \array_slice( $attachments, 0, $limit ) : NULL;
}
/**
@@ -1593,12 +1752,13 @@ abstract class _Comment extends \IPS\Content
* @apiresponse string content The content
* @apiresponse bool hidden Is hidden?
* @apiresponse string url URL to content
* @apiresponse array reactions Array of reactions given, array key is member_id of reaction giver ([member_id => [ title, id, value, icon ]])
*/
public function apiOutput( \IPS\Member $authorizedMember = NULL )
{
$idColumn = static::$databaseColumnId;
$itemColumn = static::$databaseColumnMap['item'];
return array(
$return = array(
'id' => $this->$idColumn,
'item_id' => $this->$itemColumn,
'author' => $this->author()->apiOutput( $authorizedMember ),
@@ -1607,6 +1767,35 @@ abstract class _Comment extends \IPS\Content
'hidden' => (bool) $this->hidden(),
'url' => (string) $this->url()
);
if ( \IPS\IPS::classUsesTrait( $this, 'IPS\Content\Reactable' ) )
{
if ( $reactions = $this->reactions() )
{
$enabledReactions = \IPS\Content\Reaction::enabledReactions();
$finalReactions = [];
foreach( $reactions as $memberId => $array )
{
foreach( $array as $reaction )
{
$finalReactions[ $memberId ][] = [
'title' => $enabledReactions[ $reaction ]->_title,
'id' => $reaction,
'value' => $enabledReactions[ $reaction ]->value,
'icon' => (string) $enabledReactions[ $reaction ]->_icon->url
];
}
}
$return['reactions'] = $finalReactions;
}
else
{
$return['reactions'] = [];
}
}
return $return;
}
/**
@@ -1618,9 +1807,10 @@ abstract class _Comment extends \IPS\Content
{
$filters = parent::webhookFilters();
$item = $this->item();
/* This is kept for legacy compatibility, but starting with 4.6.13 the Content and Item class handle this inside their own webhookFilter methods */
$filters['hidden'] = ( ( (bool) $this->hidden() ) or ( (bool) $item->hidden() ) );
if ( \in_array( 'IPS\Content\Lockable', class_implements( $item ) ) )
{
$filters['locked'] = $item->locked();
+418 -105
View File
@@ -402,7 +402,9 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
}
\IPS\Db::i()->delete( 'core_content_featured', [ 'feature_content_id=? and feature_content_class=?', $this->$idColumn, \get_class( $this ) ] );
\IPS\Api\Webhook::fire( str_replace( '\\', '', \substr( \get_called_class(), 3 ) ) . '_delete', $this, $this->webhookFilters() );
parent::delete();
$this->expireWidgetCaches();
@@ -444,19 +446,22 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
}
/**
* Check comment against profanity filters
* Check comment against profanity filters but do not act on it
*
* @param bool $first Is this the first comment?
* @param bool $edit Are we editing or merging (true) or is this a new comment (false)?
* @param string|NULL $content The content to check - useful for if the content needs to be checked first, before it gets saved to the database.
* @param string|NULL|bool $title The title of the content to check, or NULL to check the current title, or FALSE to not check at all.
* @return bool Whether to send unapproved notifications
* @param bool $first Is this the first comment?
* @param bool $edit Are we editing or merging (true) or is this a new comment (false)?
* @param string|NULL $content The content to check - useful for if the content needs to be checked first, before it gets saved to the database.
* @param string|NULL|bool $title The title of the content to check, or NULL to check the current title, or FALSE to not check at all.
* @param string $autoSaveLocation The autosave location key of any editors to check, or NULL to use the default.
* @param string|NULL $autoSaveKeys The autosave keys (for new content) or attach lookup ids (for an edit) of any editors to check, or NULL to use the default.
* @param array $imageUploads Images that have been uploaded that may require moderation
* @return bool Whether to send unapproved notifications (i.e. true if the content was hidden)
*/
public function checkProfanityFilters( $first=FALSE, $edit=TRUE, $content=NULL, $title=NULL )
{
public function shouldTriggerProfanityFilters( $first=FALSE, $edit=TRUE, $content=NULL, $title=NULL, $autoSaveLocation=NULL, $autoSaveKeys=NULL, $imageUploads=[], &$hiddenByFilter = FALSE, &$itemHiddenByFilter = FALSE )
{
/* Set our content */
$content = $content ?: $this->content();
/* We need our item */
$item = $this;
if ( $this instanceof \IPS\Content\Comment )
@@ -482,31 +487,78 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
return FALSE;
}
/* And the author */
/* Check the author cannot bypass */
$member = $this->author();
/* Pass this through our profanity filters to see if it needs to be mod queued */
$sendNotifications = FALSE;
/* Do we need to recount item comments and container comments? */
$recount = FALSE;
try
if ( $member->group['g_bypass_badwords'] )
{
$hiddenByFilter = FALSE;
/* If this is a content item that doesn't require a first comment, or a comment that isn't the first comment, this just pass the content as it stands. */
if ( !$this->hidden() AND ( ( $this instanceof \IPS\Content\Item ) OR ( $this instanceof \IPS\Content\Comment AND ( !$item::$firstCommentRequired OR ( $item::$firstCommentRequired AND !$first ) ) ) ) AND !$member->group['g_bypass_badwords'] )
return FALSE;
}
/* First, check the image scanner */
$idColumn = static::$databaseColumnId;
$itemIdColumn = $item::$databaseColumnId;
if ( \IPS\Settings::i()->ips_imagescanner_enable )
{
if ( $edit )
{
$autoSaveLocation = $autoSaveLocation ?: ( $item::$application . '_' . mb_ucfirst( $item::$module ) );
if ( !$autoSaveKeys )
{
if ( $this instanceof \IPS\Content\Comment and ( !( $this instanceof \IPS\Content\Review ) or !$first or !$item::$firstCommentRequired ) )
{
$autoSaveKeys = [ $this->attachmentIds() ];
}
else
{
$autoSaveKeys = [ [ $item->$itemIdColumn ] ];
}
}
$clauses = [];
$binds = [];
foreach ( $autoSaveKeys as $_autoSaveKeys )
{
$idLookups = [];
foreach ( range( 1, \count( $_autoSaveKeys ) ) as $i )
{
$idLookups[]= "id{$i}=?";
}
$clauses[] = '( ' . implode( ' AND ', $idLookups ) . ' )';
$binds = array_merge( $binds, $_autoSaveKeys );
}
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $content, $member );
if ( $title AND !$hiddenByFilter )
{
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $title, $member );
}
$attachmentsMapWhere = array_merge( array( 'location_key=? AND ( ' . implode( ' OR ', $clauses ) . ' )', $autoSaveLocation ), $binds );
}
/* If the comment is hidden (probably requires moderator approval) and this is the first post in the item that requires a comment (i.e. a post in a topic), then we need to set the item as requiring approval instead. */
elseif ( $item instanceof \IPS\Content\Hideable AND !$member->group['g_bypass_badwords'] AND ( $item::$firstCommentRequired and $first ) )
else
{
if ( $autoSaveKeys === NULL )
{
if ( $this instanceof \IPS\Content\Item or ( $first and $item::$firstCommentRequired ) )
{
$container = $item->containerWrapper();
$autoSaveKeys[] = 'newContentItem-' . $item::$application . '/' . $item::$module . '-' . ( $container ? $container->_id : 0 );
}
else
{
if ( $this instanceof \IPS\Content\Review )
{
$autoSaveKeys[] = 'review-' . $item::$application . '/' . $item::$module . '-' . $item->$itemIdColumn;
}
else
{
$autoSaveKeys[] = 'reply-' . $item::$application . '/' . $item::$module . '-' . $item->$itemIdColumn;
}
}
}
$itemHiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $content, $member );
if ( $title AND !$itemHiddenByFilter )
@@ -514,54 +566,142 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
$itemHiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $title, $member );
}
if ( isset( $item::$databaseColumnMap['approved'] ) and $itemHiddenByFilter )
$attachmentsMapWhere = \IPS\Db::i()->in( 'temp', array_map( 'md5', $autoSaveKeys ) );
}
foreach ( \IPS\Db::i()->select( 'attach_moderation_status', 'core_attachments', [ 'attach_id IN (?)', \IPS\Db::i()->select( 'attachment_id', 'core_attachments_map', $attachmentsMapWhere ) ] ) as $attachModerationStatus )
{
if ( $attachModerationStatus === 'pending' )
{
/* 'approved' is easy, clear and concise */
$column = $item::$databaseColumnMap['approved'];
$item->$column = 0;
$item->save();
$recount = TRUE;
if ( $this instanceof \IPS\Content\Item or ( $first and $item::$firstCommentRequired ) )
{
$itemHiddenByFilter = TRUE;
}
else
{
$hiddenByFilter = TRUE;
}
break;
}
elseif ( isset( $item::$databaseColumnMap['hidden'] ) and $itemHiddenByFilter )
}
if ( !$hiddenByFilter and !$itemHiddenByFilter )
{
foreach ( $imageUploads as $file )
{
/* 'hidden' is backwards */
$column = $item::$databaseColumnMap['hidden'];
$item->$column = 1;
$item->save();
$recount = TRUE;
if ( $file->requiresModeration )
{
if ( $this instanceof \IPS\Content\Item or ( $first and $item::$firstCommentRequired ) )
{
$itemHiddenByFilter = TRUE;
}
else
{
$hiddenByFilter = TRUE;
}
break;
}
}
}
}
if( $edit === TRUE )
/* Then pass this through our profanity and link filters */
try
{
if ( $this instanceof \IPS\Content\Item or ( $first and $item::$firstCommentRequired ) )
{
if ( !$itemHiddenByFilter )
{
$sendNotifications = TRUE;
$itemHiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $content );
}
if ( !$itemHiddenByFilter and $title )
{
$itemHiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $title );
}
}
else
{
if ( !$hiddenByFilter )
{
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $content );
}
if ( !$hiddenByFilter and $title )
{
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $title );
}
}
}
catch( \BadMethodCallException $e ) { }
if ( $hiddenByFilter === TRUE )
/* Return */
return ( $hiddenByFilter or $itemHiddenByFilter );
}
/**
* Check comment against profanity filters AND act on it
*
* @param bool $first Is this the first comment?
* @param bool $edit Are we editing or merging (true) or is this a new comment (false)?
* @param string|NULL $content The content to check - useful for if the content needs to be checked first, before it gets saved to the database.
* @param string|NULL|bool $title The title of the content to check, or NULL to check the current title, or FALSE to not check at all.
* @param string $autoSaveLocation The autosave location key of any editors to check, or NULL to use the default.
* @param string|NULL $autoSaveKeys The autosave keys (for new content) or attach lookup ids (for an edit) of any editors to check, or NULL to use the default.
* @param array $imageUploads Images that have been uploaded that may require moderation
* @return bool Whether to send unapproved notifications (i.e. true if the content was hidden)
*/
public function checkProfanityFilters( $first=FALSE, $edit=TRUE, $content=NULL, $title=NULL, $autoSaveLocation=NULL, $autoSaveKeys=NULL, $imageUploads=[] )
{
/* Check it */
$hiddenByFilter = FALSE;
$itemHiddenByFilter = FALSE;
$sendNotifications = FALSE;
$item = ( $this instanceof \IPS\Content\Item ) ? $this : $this->item();
$this->shouldTriggerProfanityFilters( $first, $edit, $content, $title, $autoSaveLocation, $autoSaveKeys, $imageUploads, $hiddenByFilter, $itemHiddenByFilter );
/* If we need to hide the item, then do that */
if ( $itemHiddenByFilter )
{
$recount = TRUE;
$sendNotifications = $edit;
/* 'approved' is easy, clear and concise */
if ( isset( $item::$databaseColumnMap['approved'] ) )
{
$column = $item::$databaseColumnMap['approved'];
$item->$column = 0;
$item->save();
}
/* 'hidden' is backwards */
elseif ( isset( $item::$databaseColumnMap['hidden'] ) )
{
$column = $item::$databaseColumnMap['hidden'];
$item->$column = 1;
$item->save();
}
}
/* If we need to hide this, then do that */
if ( $hiddenByFilter === TRUE or $itemHiddenByFilter === TRUE )
{
$sendNotifications = TRUE;
/* 'approved' is easy, clear and concise */
if ( isset( static::$databaseColumnMap['approved'] ) )
{
/* 'approved' is easy, clear and concise */
$column = static::$databaseColumnMap['approved'];
$this->$column = 0;
$this->save();
}
/* 'hidden' is backwards */
else if ( isset( static::$databaseColumnMap['hidden'] ) )
{
/* 'hidden' is backwards */
$column = static::$databaseColumnMap['hidden'];
$this->$column = 1;
$this->$column = ( $itemHiddenByFilter and $this instanceof \IPS\Content\Comment ) ? 2 : 1; # We use the special 2 flag to note it is only hidden because the parent is
$this->save();
}
/* We have to save here because the status change is relevant to resyncCommentCounts() and resyncLastComment(), and our comment object changes we just set can be overwritten when those methods are called. */
$this->save();
$sendNotifications = TRUE;
}
if ( $recount === TRUE )
/* If we did either, then recount number of comments */
if ( $itemHiddenByFilter or $hiddenByFilter )
{
$item->resyncCommentCounts();
$item->resyncLastComment();
@@ -574,6 +714,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
}
}
/* Return */
return $sendNotifications;
}
@@ -646,7 +787,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
/**
* Return a list of groups that cannot see this item
*
* @return NULL|Array
* @return NULL|array
*/
public function cannotViewGroups()
{
@@ -1235,7 +1376,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
{
$this->sendApprovedNotification();
}
/* Award points */
$itemClass = \in_array( 'IPS\Content\Comment', class_parents( \get_called_class() ) ) ? static::$itemClass : \get_called_class();
if ( $this instanceof \IPS\Content\Item )
@@ -1246,7 +1387,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
{
$this->author()->achievementAction( 'core', 'Review', $this );
}
elseif ( $this instanceof \IPS\Content\Comment and ( !$itemClass::$firstCommentRequired or $this->isFirst() ) )
elseif ( $this instanceof \IPS\Content\Comment and ( ( $itemClass::$firstCommentRequired AND ! $this->isFirst() ) OR ( ! $itemClass::$firstCommentRequired ) ) )
{
$this->author()->achievementAction( 'core', 'Comment', $this );
}
@@ -1455,19 +1596,23 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
/**
* Report
*
* @param string $reportContent Report content message from member
* @param int $reportType Report type (see constants in \IPS\core\Reports\Report
* @param string $reportContent Report content message from member
* @param int $reportType Report type (see constants in \IPS\core\Reports\Report)
* @param \IPS\Member|NULL $member Member making the report (or NULL for loggedIn())
* @return \IPS\core\Reports\Report
* @throws \UnexpectedValueException If there is a permission error - you should only call this method after checking canReport
*/
public function report( $reportContent, $reportType=1 )
public function report( $reportContent, $reportType=1, $member=NULL )
{
$member = ( $member ) ? $member : \IPS\Member::loggedIn();
/* Permission check */
if ( $this->canReport() !== TRUE )
$permCheck = $this->canReport( $member );
if ( $permCheck !== TRUE )
{
throw new \UnexpectedValueException;
throw new \UnexpectedValueException( $permCheck );
}
/* Find or create an index */
$idColumn = static::$databaseColumnId;
try
@@ -1481,7 +1626,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
$index->class = \get_called_class();
$index->content_id = $this->$idColumn;
$index->perm_id = $this->permId();
$index->first_report_by = (int) \IPS\Member::loggedIn()->member_id;
$index->first_report_by = (int) $member->member_id;
$index->first_report_date = time();
$index->last_updated = time();
$index->author = (int) $this->author()->member_id;
@@ -1502,10 +1647,10 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
$reportInsert = array(
'rid' => $index->id,
'report' => $reportContent,
'report_by' => (int) \IPS\Member::loggedIn()->member_id,
'report_by' => (int) $member->member_id,
'date_reported' => time(),
'ip_address' => \IPS\Request::i()->ipAddress(),
'report_type' => \IPS\Member::loggedIn()->member_id ? $reportType : 0
'report_type' => $member->member_id ? $reportType : 0
);
$insertID = \IPS\Db::i()->insert( 'core_rc_reports', $reportInsert );
@@ -1596,12 +1741,19 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
case 'author':
$this->$col = $newAuthor->member_id ? $newAuthor->member_id : 0;
break;
case 'author_name':
case 'edit_member_name':
/* Real name will contain the custom guest name if available or '' if not */
$this->$col = $newAuthor->member_id ? $newAuthor->name : $newAuthor->real_name;
break;
case 'edit_member_name':
/* Real name will contain the custom guest name if available or '' if not.
But we only want to update the user if oldName is the same as the current edit_member_name
So if "Bob" edited his own post, you want that to change if Bob becomes Bob2.
But if a moderator edited it, we don't want to change that name. */
if ( $oldAuthor->name == $this->$col )
{
$this->$col = $newAuthor->member_id ? $newAuthor->name : $newAuthor->real_name;
}
break;
case 'is_anon':
/* We are specifying an author so turn off is_anon column */
if ( $newAuthor->member_id )
@@ -1621,6 +1773,29 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
}
}
/**
* Get language articles for the given container
*
* @param string $itemClass The classname of this content
* @param array|NULL $containerData Basic data about the container. Only includes columns returned by container::basicDataColumns()
*/
public static function articlesFromIndexData( $itemClass, ?array $containerData ): array
{
/* Object URL */
$indefiniteArticle = static::_indefiniteArticle( $containerData );
$definiteArticle = static::_definiteArticle( $containerData );
$definiteArticleUc = static::_definiteArticle( $containerData, NULL, array( 'ucfirst' => TRUE ) );
if ( \in_array( 'IPS\Content\Comment', class_parents( \get_called_class() ) ) )
{
$indefiniteArticle = $itemClass::_indefiniteArticle( $containerData );
$definiteArticle = $itemClass::_definiteArticle( $containerData );
$definiteArticleUc = $itemClass::_definiteArticle( $containerData, NULL, array( 'ucfirst' => TRUE ) );
}
return array( 'indefinite' => $indefiniteArticle, 'definite' => $definiteArticle, 'definite_uc' => $definiteArticleUc );
}
/**
* Get HTML for search result display
*
@@ -1654,9 +1829,6 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
$itemUrl = $itemClass::urlFromIndexData( $indexData, $itemData, 'getPrefComment' );
/* Object URL */
$indefiniteArticle = static::_indefiniteArticle( $containerData );
$definiteArticle = static::_definiteArticle( $containerData );
$definiteArticleUc = static::_definiteArticle( $containerData, NULL, array( 'ucfirst' => TRUE ) );
if ( \in_array( 'IPS\Content\Comment', class_parents( \get_called_class() ) ) )
{
if ( \in_array( 'IPS\Content\Review', class_parents( \get_called_class() ) ) )
@@ -1669,17 +1841,15 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
$objectUrl = $itemUrl->setQueryString( array( 'do' => 'findComment', 'comment' => $indexData['index_object_id'] ) );
$showRepUrl = $itemUrl->setQueryString( array( 'do' => 'showReactionsComment', 'comment' => $indexData['index_object_id'] ) );
}
$indefiniteArticle = $itemClass::_indefiniteArticle( $containerData );
$definiteArticle = $itemClass::_definiteArticle( $containerData );
$definiteArticleUc = $itemClass::_definiteArticle( $containerData, NULL, array( 'ucfirst' => TRUE ) );
}
else
{
$objectUrl = $itemUrl;
$showRepUrl = $itemUrl->setQueryString( 'do', 'showReactions' );
}
$articles = array( 'indefinite' => $indefiniteArticle, 'definite' => $definiteArticle, 'definite_uc' => $definiteArticleUc );
/* Articles language */
$articles = static::articlesFromIndexData( $itemClass, $containerData );
/* Container details */
$containerUrl = NULL;
@@ -1740,7 +1910,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
*/
public static function searchResultSnippet( array $indexData, array $authorData, array $itemData, ?array $containerData, array $reputationData, $reviewRating, $view )
{
return $view == 'expanded' ? \IPS\Theme::i()->getTemplate( 'system', 'core', 'front' )->searchResultSnippet( $indexData ) : '';
return $view == 'expanded' ? \IPS\Theme::i()->getTemplate( 'system', 'core', 'front' )->searchResultSnippet( $indexData, $itemData ) : '';
}
/**
@@ -1752,17 +1922,32 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
* @param array $articles Articles language strings
* @param array $indexData Search index data
* @param array $itemData Data about the item
* @param bool $includeLinks Include links to member profile
* @return string
*/
public static function searchResultSummaryLanguage( $authorData, $articles, $indexData, $itemData )
public static function searchResultSummaryLanguage( $authorData, $articles, $indexData, $itemData, $includeLinks = TRUE )
{
$authorTemplate = \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLinkFromData( $authorData['member_id'], $authorData['name'], $authorData['members_seo_name'], $authorData['member_group_id'] ?? \IPS\Settings::i()->guest_group, NULL, $indexData['index_is_anon'] );
if( $includeLinks )
{
$authorTemplate = \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLinkFromData( $authorData['member_id'], $authorData['name'], $authorData['members_seo_name'], $authorData['member_group_id'] ?? \IPS\Settings::i()->guest_group, NULL, $indexData['index_is_anon'] );
}
else
{
$authorTemplate = \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userNameFromData( $authorData['name'], $authorData['member_group_id'] ?? \IPS\Settings::i()->guest_group, NULL, $indexData['index_is_anon'] );
}
if( \in_array( 'IPS\Content\Comment', class_parents( $indexData['index_class'] ) ) )
{
if( isset( $itemData['author'] ) )
{
$itemAuthorTemplate = \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLinkFromData( $itemData['author']['member_id'], $itemData['author']['name'], $itemData['author']['members_seo_name'], $itemData['author']['member_group_id'] ?? \IPS\Settings::i()->guest_group );
if( $includeLinks )
{
$itemAuthorTemplate = \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLinkFromData( $itemData['author']['member_id'], $itemData['author']['name'], $itemData['author']['members_seo_name'], $itemData['author']['member_group_id'] ?? \IPS\Settings::i()->guest_group );
}
else
{
$itemAuthorTemplate = \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userNameFromData( $itemData['author']['name'], $itemData['author']['member_group_id'] ?? \IPS\Settings::i()->guest_group );
}
}
if( \in_array( 'IPS\Content\Review', class_parents( $indexData['index_class'] ) ) )
@@ -1930,6 +2115,75 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
{
return \IPS\Email::template( 'core', '_genericContent', $type, array( $this, $includeLinks, $includeAuthor, $email ) );
}
/**
* Returns "Foo posted {{indefart}} in {{container}}, {{date}}
*
* @param \IPS\Lang|NULL $plaintextLanguage If specified, will return plaintext (not linking the user or the container in the language specified). If NULL, returns with links based on logged in user's theme and language
* @note This function was extracted from IPS\Promote
* @return string
*/
public function objectMetaDescription( $plaintextLanguage=NULL )
{
$object = $this;
$author = $this->author();
if ( $object instanceof \IPS\Content\Item )
{
$container = $object->containerWrapper();
if ( $container )
{
if ( !$plaintextLanguage )
{
return \IPS\Member::loggedIn()->language()->addToStack( 'promote_metadescription_container', FALSE, array(
'htmlsprintf' => array( $author->link(), \IPS\DateTime::ts( $this->mapped('date') )->html( FALSE ) ),
'sprintf' => array( $object->indefiniteArticle(), $container->url(), $container->_title ),
) );
}
else
{
return $plaintextLanguage->addToStack( 'promote_metadescription_container_nolink', FALSE, array(
'sprintf' => array( $object->indefiniteArticle( $plaintextLanguage ), $container->getTitleForLanguage( $plaintextLanguage ), $author->name, \IPS\DateTime::ts( $this->mapped('date') ) ),
) );
}
}
else
{
if ( !$plaintextLanguage )
{
return \IPS\Member::loggedIn()->language()->addToStack( 'promote_metadescription_nocontainer', FALSE, array(
'htmlsprintf' => array( $author->link(), \IPS\DateTime::ts( $this->mapped('date') )->html( FALSE ) ),
'sprintf' => array( $object->indefiniteArticle() )
) );
}
else
{
return $plaintextLanguage->addToStack( 'promote_metadescription_nocontainer', FALSE, array(
'sprintf' => array( $object->indefiniteArticle( $plaintextLanguage ), $author->name, \IPS\DateTime::ts( $this->mapped('date') ) )
) );
}
}
}
else if ( $object instanceof \IPS\Content\Comment )
{
if ( !$plaintextLanguage )
{
return \IPS\Member::loggedIn()->language()->addToStack( 'promote_metadescription_nocontainer', FALSE, array(
'htmlsprintf' => array( $author->link(), \IPS\DateTime::ts( $this->mapped('date') )->html( FALSE ) ),
'sprintf' => array( $object->indefiniteArticle() )
) );
}
else
{
return $plaintextLanguage->addToStack( 'promote_metadescription_nocontainer', FALSE, array(
'sprintf' => array( $object->indefiniteArticle( $plaintextLanguage ), $author->name, \IPS\DateTime::ts( $this->mapped('date') ) )
) );
}
}
throw new \OutofRangeException('object_not_valid');
}
/**
* Get a count of the database table
@@ -2046,8 +2300,17 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
}
$followIds = array();
$followers = iterator_to_array( $this->notificationRecipients( array( $offset, static::NOTIFICATIONS_PER_BATCH ), $extra ) );
$followers = $this->notificationRecipients( array( $offset, static::NOTIFICATIONS_PER_BATCH ), $extra );
/* If $followers is NULL (which can be the case if the follows are just people following the author), just return as there is nothing to do */
if ( $followers === NULL )
{
return NULL;
}
/* If we're still here, our iterator may not necessarily be one that implements Countable so we need to convert it to an array. */
$followers = iterator_to_array( $followers );
if( !\count( $followers ) )
{
return NULL;
@@ -2351,21 +2614,28 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
$item = $this;
}
/* We have to send a limit even though we want all records because otherwise the Database store does not return all columns */
foreach( \IPS\Session\Store::i()->getOnlineUsers( 0, 'desc', array( 0, 5000 ), NULL, TRUE ) as $session )
/** Item::url() can throw a LogicException exception in specific cases like when a Pages Record has no valid page */
try
{
if( mb_strpos( $session['location_url'], (string) $item->url() ) === 0 )
/* We have to send a limit even though we want all records because otherwise the Database store does not return all columns */
foreach( \IPS\Session\Store::i()->getOnlineUsers( 0, 'desc', array( 0, 5000 ), NULL, TRUE ) as $session )
{
$sessionData = $session;
$sessionData['location_url'] = NULL;
$sessionData['location_lang'] = NULL;
$sessionData['location_data'] = json_encode( array() );
$sessionData['current_id'] = 0;
$sessionData['location_permissions'] = 0;
if( mb_strpos( $session['location_url'], (string) $item->url() ) === 0 )
{
$sessionData = $session;
$sessionData['location_url'] = NULL;
$sessionData['location_lang'] = NULL;
$sessionData['location_data'] = json_encode( array() );
$sessionData['current_id'] = 0;
$sessionData['location_permissions'] = 0;
\IPS\Session\Store::i()->updateSession( $sessionData );
\IPS\Session\Store::i()->updateSession( $sessionData );
}
}
}
catch( \LogicException $e ){}
}
/**
@@ -2709,58 +2979,90 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
{
$filters = array();
$filters['author'] = $this->author()->member_id;
/* All our Zapier Triggers for content have a hidden setting, so we're using this one global for items and comments */
$filters['hidden'] = (bool) $this->hidden();
return $filters;
}
/**
* Set anonymous state
*
* @param bool $state The state, TRUE for anonymous FALSE for not
* @param \IPS\Member|NULL $member The member posting anonymously or NULL for logged in member
* @param bool $state The state, TRUE for anonymous FALSE for not
* @param \IPS\Member|NULL $member The member posting anonymously or NULL for logged in member
* @return void
*/
public function setAnonymous( $state = TRUE, $member = NULL )
public function setAnonymous( bool $state = TRUE, \IPS\Member $member = NULL)
{
if( !$this instanceof \IPS\Content\Anonymous or !$this->container()->canPostAnonymously() )
if( !$this instanceof \IPS\Content\Anonymous )
{
throw new \BadMethodCallException;
throw new \BadMethodCallException();
}
$member = $member ?: \IPS\Member::loggedIn();
if( $state == $this->isAnonymous() )
{
return;
}
$class = \get_class( $this );
$idColumn = static::$databaseColumnId;
$anonColumn = static::$databaseColumnMap['is_anon'];
if( !$state )
{
try
{
$originalAuthor = \IPS\DB::i()->select( 'anonymous_member_id', 'core_anonymous_posts', array( 'anonymous_object_class=? and anonymous_object_id=?', $class, $this->$idColumn ) )->first();
$member = \IPS\Member::load( $originalAuthor );
}
catch (\UnderflowException $e )
{
$member = NULL;
}
}
$member = $member ?: \IPS\Member::loggedIn();
if( $state and !$this->container()->canPostAnonymously( 0, $member ) )
{
throw new \BadMethodCallException();
}
if( $state )
{
{
/* Insert the anonymous map */
$save = array(
'anonymous_member_id' => $member->member_id,
'anonymous_member_id' => ( $this->author()->member_id ) ? $this->author()->member_id : $member->member_id,
'anonymous_object_class' => $class,
'anonymous_object_id' => $this->$idColumn
);
\IPS\Db::i()->replace( 'core_anonymous_posts', $save );
$member = new \IPS\Member;
$this->$anonColumn = 1;
}
else
{
\IPS\Db::i()->delete( 'core_anonymous_posts', array( 'anonymous_member_id=? and anonymous_object_class=? and anonymous_object_id=?', $member->member_id, $class, $this->$idColumn ) );
\IPS\Db::i()->delete( 'core_anonymous_posts', array( 'anonymous_object_class=? and anonymous_object_id=?', $class, $this->$idColumn ) );
$this->$anonColumn = 0;
}
/* @todo only run the rest of the code here if the $anonColumn state is different from original. Waste of processing otherwise */
$this->save();
$this->changeAuthor( $member, FALSE );
if ( \in_array( 'IPS\Content\Comment', class_parents( $this ) ) )
{
$this->item()->rebuildFirstAndLastCommentData();
}
$this->expireWidgetCaches();
}
/**
* Is this an anonymous entry?
*
@@ -2770,4 +3072,15 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
{
return $this->mapped('is_anon');
}
/**
* Returns the content images
*
* @return array|NULL
* @throws \BadMethodCallException
*/
public function imageLabelsForSearch()
{
return [];
}
}
+201 -74
View File
@@ -47,7 +47,25 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
/* Ensure JS loaded for forms/content functions such as moderation */
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'front_core.js', 'core' ) );
return parent::execute();
parent::execute();
/* Do this to prevent non-existent and non-accessible items from returning a status of 200 */
if ( empty( \IPS\Output::i()->output ) AND !\IPS\Request::i()->_bypassItemIdCheck AND isset( \IPS\Request::i()->id ) )
{
$contentModel = static::$contentModel;
try
{
$item = $contentModel::load( \IPS\Request::i()->id );
if ( !$item->can( 'read' ) )
{
\IPS\Output::i()->error( 'node_error', '2S136/1X', 403, '' );
}
}
catch ( \OutOfRangeException $e )
{
\IPS\Output::i()->error( 'node_error', '2S136/1Y', 404, '' );
}
}
}
/**
@@ -132,7 +150,15 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
{
\IPS\Output::i()->redirect( $redirectToUrl );
}
/* Get ready to store data layer info */
$dataLayer = \IPS\Settings::i()->core_datalayer_enabled;
$dataLayerProperties = array();
if ( $dataLayer )
{
$dataLayerProperties = $item->getDataLayerProperties();
}
/* Check we're on a valid page */
$paginationType = 'comment';
if ( isset( \IPS\Request::i()->tab ) and \IPS\Request::i()->tab === 'reviews' )
@@ -168,6 +194,8 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
$lastPageMethod = 'last' . mb_ucfirst( $paginationType ) . 'PageUrl';
\IPS\Output::i()->redirect( $item->$lastPageMethod(), NULL, 303 );
}
$dataLayerProperties['page_number'] = \intval( \IPS\Request::i()->page );
}
/* Add rel tags */
@@ -211,10 +239,21 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
try
{
\IPS\Redis::i()->zIncrBy( 'topic_views', 1, $class .'__' . $item->$idColumn );
$countUpdated = true;
}
catch( \Exception $e ) {}
}
if( \IPS\Application::appIsEnabled( 'cloud' ) and \IPS\cloud\Realtime::i()->isEnabled('trending') and $class::$includeInTrending )
{
try
{
/* Score by timestamp for trending */
\IPS\Redis::i()->zIncrBy( 'trending', time() * 0.1, $class .'__' . $item->$idColumn );
}
catch( \Exception $e ) {}
}
if ( ! $countUpdated )
{
@@ -262,7 +301,21 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
'item_type' => isset( $class::$contentType ) ? $class::$contentType : $class::$title,
'category_name' => isset( $item::$databaseColumnMap['container'] ) ? $item->container()->_title : NULL
);
/* Data Layer */
if ( $dataLayer )
{
if ( !\IPS\Request::i()->isAjax() )
{
\IPS\core\DataLayer::i()->addEvent( 'content_view', $dataLayerProperties );
}
unset( $dataLayerProperties['ips_key'] );
foreach ( $dataLayerProperties as $key => $value )
{
\IPS\core\DataLayer::i()->addContextProperty( $key, $value );
}
}
if( $item->mapped( 'updated' ) OR $item->mapped( 'last_comment' ) OR $item->mapped( 'last_review' ) )
{
\IPS\Output::i()->metaTags['og:updated_time'] = \IPS\DateTime::ts( $item->mapped( 'updated' ) ? $item->mapped( 'updated' ) : ( $item->mapped( 'last_comment' ) ? $item->mapped( 'last_comment' ) : $item->mapped( 'last_review' ) ) )->rfc3339();
@@ -356,6 +409,14 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
$item = $class::loadAndCheckPerms( \IPS\Request::i()->id );
$commentClass = $class::$commentClass;
/* Don't auto-poll archived content, it never gets updated */
if( isset( $item::$archiveClass ) AND method_exists( $item, 'isArchived' ) AND $item->isArchived() )
{
\IPS\Output::i()->json( array( 'error' => 'auto_polling_disabled' ) );
return;
}
$commentIdColumn = $commentClass::$databaseColumnId;
$commentDateColumn = $commentClass::$databaseColumnMap['date'];
@@ -514,7 +575,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
$item->processAfterEdit( $values );
/* Moderator log */
$toLog = array( $item::$title => FALSE, $item->url()->__toString() => FALSE, $item::$title => TRUE, $item->mapped( 'title' ) => FALSE );
$toLog = array( $item::$title => TRUE, $item->url()->__toString() => FALSE, $item::$title => TRUE, $item->mapped( 'title' ) => FALSE );
if ( $oldTitle != $item->$titleField )
{
@@ -723,12 +784,12 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
$newTitle = new \IPS\Helpers\Form\Text( 'newTitle', \IPS\Request::i()->newTitle );
if( $newTitle->validate() !== TRUE or $newTitle->value !== \IPS\Request::i()->newTitle )
if( $newTitle->validate() !== TRUE )
{
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack( 'form_tags_not_allowed', FALSE, array( 'sprintf' => array( \IPS\Request::i()->newTitle ) ) ) );
}
$item->$titleField = \IPS\Request::i()->newTitle;
$item->$titleField = $newTitle->value;
$idField = $item::$databaseColumnId;
$item->save();
@@ -761,7 +822,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
\IPS\Session::i()->modLog( 'modlog__comment_edit_title', array( (string) $item->url() => FALSE, $item->$titleField => FALSE, $oldTitle => FALSE ), $item );
}
\IPS\Output::i()->json( 'OK' );
\IPS\Output::i()->json( $item->$titleField );
}
catch( \LogicException $e )
{
@@ -1557,7 +1618,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
/* If we do not have a time last read set for this content, fallback to the reset time */
$resetTimes = \IPS\Member::loggedIn()->markersResetTimes( $class::$application );
if ( array_key_exists( $item->container()->_id, $resetTimes ) and $item->mapped('date') < $resetTimes[ $item->container()->_id ] )
if ( ( \is_array( $resetTimes ) AND array_key_exists( $item->container()->_id, $resetTimes ) ) and $item->mapped('date') < $resetTimes[ $item->container()->_id ] )
{
$comment = $item->comments( 1, NULL, 'date', 'asc', NULL, NULL, \IPS\DateTime::ts( $resetTimes[ $item->container()->_id ] ) );
@@ -1597,7 +1658,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
$error = 'node_error';
}
\IPS\Output::i()->error( $error, '2S136/V', 404, '' );
\IPS\Output::i()->error( $error, '2S136/V', 403, '' );
}
catch( \LogicException $e )
{
@@ -1666,7 +1727,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
$error = 'node_error';
}
\IPS\Output::i()->error( $error, '2S136/Q', 404, '' );
\IPS\Output::i()->error( $error, '2S136/Q', 403, '' );
}
}
@@ -1708,7 +1769,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
}
catch( \LogicException $e )
{
\IPS\Output::i()->error( 'node_error', '2S136/X', 404, '' );
\IPS\Output::i()->error( 'node_error', '2S136/X', 403, '' );
}
}
@@ -1909,7 +1970,8 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
}
$form = new \IPS\Helpers\Form;
$form->add( new \IPS\Helpers\Form\YesNo( 'message_is_public', isset( $current['is_public'] ) ? $current['is_public'] : TRUE ) );
$form->attributes['data-controller'] = 'core.front.core.contentMessage';
$form->add( new \IPS\Helpers\Form\YesNo( 'message_is_public', isset( $current['is_public'] ) ? $current['is_public'] : FALSE ) );
$form->add( new \IPS\Helpers\Form\Editor( 'message', $current ? $current['message'] : NULL , TRUE, array( 'app' => 'core', 'key' => 'Meta', 'autoSaveKey' => $current ? "meta-message-" . \IPS\Request::i()->meta_id : "meta-message-new", 'attachIds' => $current ? array( \IPS\Request::i()->meta_id, NULL, 'core_ContentMessages' ) : NULL ) ) );
$form->add( new \IPS\Helpers\Form\Custom( 'message_color', isset( $current['color'] ) ? $current['color'] : 'none', FALSE, array( 'getHtml' => function( $element )
{
@@ -2083,7 +2145,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
if ( \IPS\Member::loggedIn()->modPermission('can_set_best_answer') )
{
\IPS\Session::i()->modLog( 'modlog__best_answer_unset', array( $item->$solvedField => FALSE ), $item );
\IPS\Session::i()->modLog( 'modlog__best_answer_unset', array( $comment->$idField => FALSE ), $item );
}
}
catch ( \Exception $e ) {}
@@ -2248,7 +2310,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
}
}
}
$commentPosition = \IPS\Db::i()->select( 'COUNT(*) AS position', $commentClass::$databaseTable, $where )->first();
$commentPosition = $commentClass::db()->select( 'COUNT(*) AS position', $commentClass::$databaseTable, $where )->first();
/* Now work out what page that makes it */
$url = $item->url();
@@ -2381,7 +2443,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
if ( isset( \IPS\Request::i()->restoreAsHidden ) )
{
\IPS\Session::i()->modLog( 'modlog__action_restore_hidden', array(
$comment::$title => FALSE,
$comment::$title => TRUE,
$comment->url()->__toString() => FALSE
) );
@@ -2390,7 +2452,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
else
{
\IPS\Session::i()->modLog( 'modlog__action_restore', array(
$comment::$title => FALSE,
$comment::$title => TRUE,
$comment->url()->__toString() => FALSE
) );
@@ -2452,7 +2514,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
/* Post Anonymously */
$container = $item->containerWrapper();
if ( $container and $container->canPostAnonymously( $container::ANON_COMMENTS ) and $comment instanceof \IPS\Content\Anonymous and \IPS\Member::loggedIn()->member_id and \IPS\Member::loggedIn()->group['gbw_can_post_anonymously'] )
if ( $container and $container->canPostAnonymously( $container::ANON_COMMENTS ) and $comment instanceof \IPS\Content\Anonymous and ( $comment->author() and $comment->author()->group['gbw_can_post_anonymously'] or $comment->isAnonymous() ) )
{
$form->add ( new \IPS\Helpers\Form\YesNo( 'post_anonymously', $comment->isAnonymous(), FALSE, array( 'label' => \IPS\Member::loggedIn()->language()->addToStack( 'post_anonymously_suffix' ) ), NULL, NULL, NULL, 'post_anonymously' ) );
}
@@ -2545,11 +2607,17 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
$comment->editRating( $values['rating_value'] );
}
/* Anonymous posting */
if( isset( $values[ 'post_anonymously' ] ) )
{
$comment->setAnonymous( $values[ 'post_anonymously' ] );
/* The anon changes to the user details (photo, name, etc) is outside the JS scope to change when editing a comment */
if ( (bool) $comment->isAnonymous() !== (bool) $values['post_anonymously'] )
{
$reloadPage = true;
}
$comment->setAnonymous( $values[ 'post_anonymously' ], $comment->author() );
}
/* Moderator log */
@@ -2770,12 +2838,18 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
\IPS\Application::load('core')->extensions( 'core', 'MetaData' )['FeaturedComments']->unfeatureComment( $oldItem, $comment );
}
/* Remove solved associations */
if ( $oldItem and \IPS\IPS::classUsesTrait( $oldItem, 'IPS\Content\Solvable' ) and $oldItem->isSolved() and ( $oldItem->mapped('solved_comment_id') === $comment->$commentIdColumn ) )
{
$oldItem->toggleSolveComment( $comment->$commentIdColumn, FALSE );
}
$comment->move( $item );
$oldItem->rebuildFirstAndLastCommentData();
/* Log it */
\IPS\Session::i()->modLog( 'modlog__action_split', array(
$item::$title => FALSE,
$item::$title => TRUE,
$item->url()->__toString() => FALSE,
$item->mapped( 'title' ) => FALSE,
$oldItem->url()->__toString() => FALSE,
@@ -3239,7 +3313,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
/* Create a copy of the old item for logging */
$oldItem = $item;
$comments = new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select(
$comments = iterator_to_array( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select(
'*',
$commentClass::$databaseTable,
array(
@@ -3247,7 +3321,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
\IPS\Db::i()->in( $commentClass::$databasePrefix . $commentClass::$databaseColumnId, array_keys( \IPS\Request::i()->multimod ) )
),
$commentClass::$databasePrefix . $commentClass::$databaseColumnMap['date']
), $commentClass );
), $commentClass ) );
foreach ( $comments as $comment )
{
@@ -3322,6 +3396,12 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
\IPS\Application::load('core')->extensions( 'core', 'MetaData' )['FeaturedComments']->unfeatureComment( $oldItem, $comment );
}
/* Remove solved associations */
if ( $oldItem and \IPS\IPS::classUsesTrait( $oldItem, 'IPS\Content\Solvable' ) and $oldItem->isSolved() and ( $oldItem->mapped('solved_comment_id') === $comment->$commentIdColumn ) )
{
$oldItem->toggleSolveComment( $comment->$commentIdColumn, FALSE );
}
if( $comment == $firstComment AND $comment->hidden() !== 0 )
{
if ( isset( $comment::$databaseColumnMap['hidden'] ) )
@@ -3364,7 +3444,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
/* Log it */
\IPS\Session::i()->modLog( 'modlog__action_split', array(
$item::$title => FALSE,
$item::$title => TRUE,
$item->url()->__toString() => FALSE,
$item->mapped( 'title' ) => FALSE,
$oldItem->url()->__toString() => FALSE,
@@ -3460,7 +3540,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
/* Log it */
\IPS\Session::i()->modLog( 'modlog__action_merge_comments', array(
$firstComment::$title => FALSE,
$firstComment::$title => TRUE,
$firstComment->url()->__toString() => FALSE,
$firstComment->$commentIdColumn => FALSE,
), $item );
@@ -3751,64 +3831,96 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
{
$class = static::$contentModel;
$item = $class::loadAndCheckPerms( \IPS\Request::i()->id );
$items = [];
if( !$item instanceof \IPS\Content\Tags OR ( $item->tags() === NULL AND $item->prefix( FALSE ) === NULL ) )
/* Are we using elasticsearch? */
if ( \IPS\Settings::i()->search_method == 'elastic' )
{
return NULL;
}
$query = \IPS\Content\Search\Query::init( \IPS\Member::loggedIn() );
$query->resultsToGet = $limit;
$query->filterByMoreLikeThis( $item );
$query->filterByLastUpdatedDate( \IPS\DateTime::create()->sub( new \DateInterval( 'P2Y' ) ) );
$results = $query->search();
$results->init();
/* Store tags in array, so that we can add a prefix if set */
$tags = $item->tags() ?: array();
$tags[] = $item->prefix( FALSE );
/* Build the where clause */
$where = array(
array( '(' . \IPS\Db::i()->in( 'tag_text', $tags ) . ')' ),
array( '!(tag_meta_app=? and tag_meta_area=? and tag_meta_id=?)', $class::$application, $class::$module, \IPS\Request::i()->id ),
array( '(' . \IPS\Db::i()->findInSet( 'tag_perm_text', \IPS\Member::loggedIn()->groups ) . ' OR ' . 'tag_perm_text=? )', '*' ),
array( 'tag_perm_visible=1' )
);
/* Allow the item to manipulate the query if needed */
if( $item->similarContentFilter() )
{
$where = array_merge( $where, $item->similarContentFilter() );
}
$select = \IPS\Db::i()->select(
'tag_meta_app,tag_meta_area,tag_meta_id',
'core_tags',
$where,
'tag_added DESC',
array( 0, $limit ),
array( 'tag_meta_app', 'tag_meta_area', 'tag_meta_id', 'tag_added' )
)->join(
'core_tags_perms',
array( 'tag_perm_aai_lookup=tag_aai_lookup' )
);
$items = array();
foreach( $select as $result )
{
foreach( \IPS\Application::load( $result['tag_meta_app'] )->extensions( 'core', 'ContentRouter' ) as $key => $router )
foreach( $results as $result )
{
foreach( $router->classes AS $itemClass )
$object = $result->asArray()['indexData'];
$class = $object['index_class'];
$loaded = $class::loadAndCheckPerms( $object['index_object_id'] );
if ( $loaded instanceof \IPS\Content\Item )
{
if( $itemClass::$module == $result['tag_meta_area'] )
{
try
{
$items[ $result['tag_meta_id'] ] = $itemClass::loadAndCheckPerms( $result['tag_meta_id'] );
break;
}
catch( \Exception $e ){}
}
$obj = $loaded;
}
else
{
$obj = $loaded->item();
}
$items[] = $obj;
}
}
else
{
if ( !$item instanceof \IPS\Content\Tags or ( $item->tags() === NULL and $item->prefix( FALSE ) === NULL ) )
{
return NULL;
}
/* Store tags in array, so that we can add a prefix if set */
$tags = $item->tags() ?: array();
$tags[] = $item->prefix( FALSE );
/* Build the where clause */
$where = array(
array('(' . \IPS\Db::i()->in( 'tag_text', $tags ) . ')'),
array('!(tag_meta_app=? and tag_meta_area=? and tag_meta_id=?)', $class::$application, $class::$module, \IPS\Request::i()->id),
array('(' . \IPS\Db::i()->findInSet( 'tag_perm_text', \IPS\Member::loggedIn()->groups ) . ' OR ' . 'tag_perm_text=? )', '*'),
array('tag_perm_visible=1')
);
/* Allow the item to manipulate the query if needed */
if ( $item->similarContentFilter() )
{
$where = array_merge( $where, $item->similarContentFilter() );
}
$select = \IPS\Db::i()->select(
'tag_meta_app,tag_meta_area,tag_meta_id',
'core_tags',
$where,
'tag_added DESC',
array(0, $limit),
array('tag_meta_app', 'tag_meta_area', 'tag_meta_id', 'tag_added')
)->join(
'core_tags_perms',
array('tag_perm_aai_lookup=tag_aai_lookup')
);
foreach ( $select as $result )
{
foreach ( \IPS\Application::load( $result['tag_meta_app'] )->extensions( 'core', 'ContentRouter' ) as $key => $router )
{
foreach ( $router->classes as $itemClass )
{
if ( $itemClass::$module == $result['tag_meta_area'] )
{
try
{
$items[$result['tag_meta_id']] = $itemClass::loadAndCheckPerms( $result['tag_meta_id'] );
break;
}
catch ( \Exception $e )
{
}
}
}
}
}
}
return $items;
}
catch ( \Exception $e )
@@ -3905,7 +4017,12 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
}
catch ( \UnderflowException $e )
{
\IPS\Output::i()->error( 'node_error', '2S136/1W', 403, '' );
if( \IPS\Request::i()->isAjax() )
{
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->basicHover( \IPS\Member::loggedIn()->language()->addToStack( 'anon_user_deleted' ) );
return;
}
\IPS\Output::i()->error( 'anon_user_deleted', '2S136/1W', 403, '' );
}
$author = \IPS\Member::load( $anonymousAuthor );
@@ -3935,4 +4052,14 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
{
$this->reveal();
}
/**
* A convenient hook point to finish any set up in manage()
*
* @param \IPS\Content\Item $item The item that is being set up in manage()
* @return void
*/
protected function finishManage( $item )
{
}
}
+269 -53
View File
@@ -54,12 +54,22 @@ abstract class _Item extends \IPS\Content
* @brief [Content\Item] Include the ability to search this content item in global site searches
*/
public static $includeInSiteSearch = TRUE;
/**
* @brief [Content\Item] Include these items in trending content
*/
public static $includeInTrending = TRUE;
/**
* @brief [Content\Item] Sharelink HTML
*/
protected $sharelinks = array();
/**
* @brief [Content\Item] Group Posted cache
*/
public $groupsPosted = [];
/**
* Whether or not to include in site search
*
@@ -387,7 +397,7 @@ abstract class _Item extends \IPS\Content
{
$member->name = $values['guest_name'];
}
/* Create the item */
$time = ( static::canFuturePublish( NULL, $container ) and isset( static::$databaseColumnMap['date'] ) and isset( $values[ static::$formLangPrefix . 'date' ] ) and $values[ static::$formLangPrefix . 'date' ] instanceof \IPS\DateTime ) ? $values[ static::$formLangPrefix . 'date' ] : new \IPS\DateTime;
@@ -397,6 +407,12 @@ abstract class _Item extends \IPS\Content
$obj->processForm( $values );
$obj->save();
/* It is possible that $member has changed */
if( $obj->author()->member_id != $member->member_id )
{
$member = $obj->author();
}
/* Create the comment */
$comment = NULL;
if ( isset( static::$commentClass ) and static::$firstCommentRequired )
@@ -418,7 +434,7 @@ abstract class _Item extends \IPS\Content
$obj->save();
}
}
/* Update posts per day limits - don't do this for content items that require a first comment as the comment class will handle that */
if ( $member->member_id AND $member->group['g_ppd_limit'] AND static::$firstCommentRequired === FALSE )
{
@@ -433,12 +449,11 @@ abstract class _Item extends \IPS\Content
$member->members_day_posts = $current;
$member->save();
}
/* Post anonymously */
if( isset( $values[ 'post_anonymously' ] ) )
{
$obj->setAnonymous( $values[ 'post_anonymously' ] );
$obj->setAnonymous( $values[ 'post_anonymously' ], $member );
}
/* Do any processing */
@@ -770,7 +785,7 @@ abstract class _Item extends \IPS\Content
}
/* Post Anonymously */
if ( $container and $container->canPostAnonymously( $container::ANON_ITEMS ) )
if ( $container and $container->canPostAnonymously( $container::ANON_ITEMS ) and ( $item === NULL or ( $item->author() and $item->author()->group['gbw_can_post_anonymously'] ) or $item->isAnonymous() ) )
{
$return['post_anonymously'] = new \IPS\Helpers\Form\YesNo( 'post_anonymously', ( $item ) ? $item->isAnonymous() : FALSE , FALSE, array( 'label' => \IPS\Member::loggedIn()->language()->addToStack( 'post_anonymously_suffix' ) ), NULL, NULL, NULL, 'post_anonymously' );
}
@@ -997,7 +1012,15 @@ abstract class _Item extends \IPS\Content
* @param array $values Values from form
* @return void
*/
protected function processBeforeCreate( $values ) {}
protected function processBeforeCreate( $values ) {
/* Check for banned IP - The banned ip addresses are only checked inside the register and login controller, so people are able to bypass them when PBR is used */
if( !\IPS\Member::loggedIn()->member_id AND \IPS\Request::i()->ipAddressIsBanned() )
{
\IPS\Output::i()->showBanned();
}
}
/**
* Process created object AFTER the object has been created
@@ -1022,6 +1045,32 @@ abstract class _Item extends \IPS\Content
{
\IPS\Api\Webhook::fire( str_replace( '\\', '', \substr( \get_called_class(), 3 ) ) . '_create', $this, $this->webhookFilters() );
}
/* Data Layer Event */
if ( \IPS\Settings::i()->core_datalayer_enabled )
{
/* Is it a status update? */
if ( isset( static::$title ) and static::$title === \IPS\core\Statuses\Status::$title )
{
$member = \IPS\Member::load( $this->member_id );
$properties = array(
'profile_id' => $member->member_id,
'profile_name' => $member->real_name ?: null,
);
\IPS\core\DataLayer::i()->addEvent( 'social_update', $properties );
}
/* Otherwise, make sure it isn't a PM Conversation */
elseif ( !isset( static::$title ) or static::$title !== \IPS\core\Messenger\Conversation::$title )
{
\IPS\core\DataLayer::i()->addEvent( 'content_create', $this->getDataLayerProperties() );
}
}
/* Send this URL to IndexNow if the guest can view it */
if ( $this->canView( new \IPS\Member ) )
{
\IPS\core\IndexNow::addUrlToQueue( $this->url() );
}
}
/**
@@ -1125,10 +1174,12 @@ abstract class _Item extends \IPS\Content
{
$this->logEdit( $values );
}
$container = $this->containerWrapper();
if ( $this instanceof \IPS\Content\FuturePublishing AND isset( $values[ static::$formLangPrefix . 'date' ] ) )
{
$container = $this->containerWrapper();
$column = static::$databaseColumnMap['is_future_entry'];
if ( $container AND $this->$column )
@@ -1150,10 +1201,17 @@ abstract class _Item extends \IPS\Content
}
/* Post anonymously */
if( isset( $values[ 'post_anonymously' ] ) and ( isset( $container ) and $container->canPostAnonymously( $container::ANON_ITEMS ) ) )
if( isset( $values[ 'post_anonymously' ] ) and ( $container and $container->canPostAnonymously( $container::ANON_ITEMS ) ) )
{
$this->setAnonymous( $values[ 'post_anonymously' ] );
$this->setAnonymous( $values[ 'post_anonymously' ], $this->author() );
}
/* Send this URL to IndexNow if the guest can view it */
if( $this->canView( new \IPS\Member ) )
{
\IPS\core\IndexNow::addUrlToQueue( $this->url() );
}
}
/* Holds the old content for edit logging */
@@ -1354,7 +1412,7 @@ abstract class _Item extends \IPS\Content
*/
public function url( $action=NULL )
{
if( $action == 'getPrefComment' )
if( $action === 'getPrefComment' AND \IPS\Member::loggedIn()->member_id )
{
$pref = \IPS\Member::loggedIn()->linkPref() ?: \IPS\Settings::i()->link_default;
@@ -1373,7 +1431,7 @@ abstract class _Item extends \IPS\Content
break;
}
}
elseif( !\IPS\Member::loggedIn()->member_id AND $action == 'getNewComment' )
elseif( ( $action == 'getPrefComment' OR $action == 'getNewComment' OR $action == 'getLastComment' ) AND !\IPS\Member::loggedIn()->member_id )
{
$action = NULL;
}
@@ -1625,7 +1683,7 @@ abstract class _Item extends \IPS\Content
}
}
return \count( $attachments ) ? $attachments : NULL;
return \count( $attachments ) ? \array_slice( $attachments, 0, $limit ) : NULL;
}
/**
@@ -2263,7 +2321,7 @@ abstract class _Item extends \IPS\Content
$hasPrevious = TRUE;
try
{
$previousFirstComment = $commentClass::constructFromData( \IPS\Db::i()->select( '*', $commentClass::$databaseTable, array( $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['item'] . '=? AND ' . $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['first'] . '=?', $this->$idColumn, TRUE ) )->first() );
$previousFirstComment = $commentClass::constructFromData( \IPS\Db::i()->select( '*', $commentClass::$databaseTable, array( $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['item'] . '=? AND ' . $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['first'] . '=?', $this->$idColumn, TRUE ), NULL, 1 )->first() );
}
catch( \UnderflowException $e )
{
@@ -2717,6 +2775,14 @@ abstract class _Item extends \IPS\Content
{
\IPS\Member::load( $member )->recountNotifications();
}
/** Item::url() can throw a LogicException exception in specific cases like when a Pages Record has no valid page */
try
{
\IPS\core\IndexNow::addUrlToQueue( $this->url() );
}
catch( \LogicException $e ) {}
}
/**
@@ -4343,39 +4409,47 @@ abstract class _Item extends \IPS\Content
/* Solved count */
$commentClass = static::$commentClass;
if ( \IPS\IPS::classUsesTrait( $this, 'IPS\Content\Solvable' ) )
$showSolvedStats = FALSE;
if ( method_exists( $this, 'isQuestion' ) and $this->isQuestion() )
{
if ( $this->containerAllowsMemberSolvable() OR $this->containerAllowsSolvable() )
$showSolvedStats = TRUE;
}
else if ( \IPS\IPS::classUsesTrait( $this, 'IPS\Content\Solvable' ) and ( $this->containerAllowsMemberSolvable() OR $this->containerAllowsSolvable() ) )
{
$showSolvedStats = TRUE;
}
if ( $commentClass AND $showSolvedStats )
{
$memberIds = array();
$solvedCounts = array();
$authorField = $commentClass::$databaseColumnMap['author'];
foreach( $results as $id => $data )
{
$memberIds = array();
$solvedCounts = array();
$authorField = $commentClass::$databaseColumnMap['author'];
$memberIds[ $data->$authorField ] = $data->$authorField;
}
if ( \count( $memberIds ) )
{
foreach( \IPS\Db::i()->select( 'COUNT(*) as count, member_id', 'core_solved_index', array( \IPS\Db::i()->in( 'member_id', $memberIds ) ), NULL, NULL, 'member_id' ) as $member )
{
$solvedCounts[ $member['member_id'] ] = $member['count'];
}
foreach( $results as $id => $data )
{
$memberIds[ $data->$authorField ] = $data->$authorField;
}
if ( \count( $memberIds ) )
{
foreach( \IPS\Db::i()->select( 'COUNT(*) as count, member_id', 'core_solved_index', array( \IPS\Db::i()->in( 'member_id', $memberIds ) ), NULL, NULL, 'member_id' ) as $member )
if ( isset( $solvedCounts[ $data->$authorField ] ) )
{
$solvedCounts[ $member['member_id'] ] = $member['count'];
}
foreach( $results as $id => $data )
{
if ( isset( $solvedCounts[ $data->$authorField ] ) )
{
$results[ $id ]->author_solved_count = $solvedCounts[ $data->$authorField ];
}
$results[ $id ]->author_solved_count = $solvedCounts[ $data->$authorField ];
}
}
}
}
/* Recognized content */
if ( \IPS\IPS::classUsesTrait( $commentClass, 'IPS\Content\Recognizable' ) )
if ( $commentClass AND \IPS\IPS::classUsesTrait( $commentClass, 'IPS\Content\Recognizable' ) )
{
foreach( \IPS\Db::i()->select( '*', 'core_member_recognize', [ [ 'r_content_class=?', $commentClass ], [ \IPS\Db::i()->in('r_content_id', array_keys( $results ) ) ] ] ) as $row )
{
@@ -4402,7 +4476,7 @@ abstract class _Item extends \IPS\Content
*/
public function mergeConcurrentComment()
{
if ( \IPS\Member::loggedIn()->member_id and \IPS\Settings::i()->merge_concurrent_posts and $this->lastCommenter()->member_id == \IPS\Member::loggedIn()->member_id )
if ( \IPS\Member::loggedIn()->member_id and \IPS\Settings::i()->merge_concurrent_posts and $this->lastCommenter()->member_id == \IPS\Member::loggedIn()->member_id and !\IPS\Member::loggedIn()->moderateNewContent() )
{
$lastComment = $this->comments( 1, 0, 'date', 'desc', NULL, TRUE );
@@ -4523,6 +4597,12 @@ abstract class _Item extends \IPS\Content
\IPS\Output::i()->error( 'posts_per_day_error', '2S177/2', 403, '' );
}
}
/* Check for banned IP - The banned ip addresses are only checked inside the register and login controller, so people are able to bypass them when PBR is used */
if( !\IPS\Member::loggedIn()->member_id AND \IPS\Request::i()->ipAddressIsBanned() )
{
\IPS\Output::i()->showBanned();
}
$currentPageCount = \IPS\Request::i()->currentPage;
@@ -4607,8 +4687,23 @@ abstract class _Item extends \IPS\Content
{
$message = \IPS\Member::loggedIn()->language()->addToStack( 'mod_queue_message' );
}
\IPS\Output::i()->json( array( 'type' => 'add', 'id' => $comment->$commentIdColumn, 'page' => $newPageCount, 'total' => $this->mapped('num_comments'), 'content' => \IPS\Output::i()->replaceEmojiWithImages( $output ), 'message' => $message ) );
/* Data Layer stuff for comments here, not in Comment class. We track user actions, and the user action is handled here */
$dataLayer = \IPS\Settings::i()->core_datalayer_enabled ? $this->getDataLayerProperties( $comment ) : array();
\IPS\Output::i()->json(
array(
'type' => 'add',
'id' => $comment->$commentIdColumn,
'page' => $newPageCount,
'total' => $this->mapped('num_comments'),
'content' => \IPS\Output::i()->replaceEmojiWithImages( $output ),
'message' => $message,
'postedByLoggedInMember' => true,
/* This is used on the front end */
"dataLayer" => $dataLayer,
)
);
}
return;
}
@@ -4667,6 +4762,102 @@ abstract class _Item extends \IPS\Content
return $this->_commentFormHtml;
}
protected $_dataLayerProperties = array();
/**
* Most, if not all of these are the same for different events, so we can just have one method
*
* @param IPS\Content\Comment $comment A comment item, leave null for these keys to be omitted
*
* @return array
*/
public function getDataLayerProperties( ?\IPS\Content\Comment $comment = null )
{
$commentIdColumn = $comment ? $comment::$databaseColumnId : null;
$index = $commentIdColumn ? ( $comment->$commentIdColumn ?: 0 ) : 0;
if ( !isset( $this->_dataLayerProperties[$index] ) )
{
$app = static::$application ?? null;
$idColumn = static::$databaseColumnId;
$dataLayer = array(
'author_id' => \IPS\core\DataLayer::i()->getSsoId( $this->author()->member_id ),
'author_name' => $this->author()->real_name ?: null,
'content_age' => $this->mapped( 'date' ) ? \intval( floor( ( time() - $this->mapped( 'date' ) ) / 86400 ) ) : null,
'content_container_id' => null,
'content_container_name' => null,
'content_container_type' => null,
'content_container_url' => null,
'content_id' => $this->$idColumn,
'content_title' => $this->mapped( 'title' ),
'content_type' => static::$contentType ?? null,
'content_url' => (string) $this->url(),
);
if ( $comment AND ( $commentIdColumn = $comment::$databaseColumnId ?? null ) )
{
$dataLayer = array_replace( $dataLayer, array(
'comment_id' => $comment->$commentIdColumn,
'comment_type' => $comment::$commentType ?? null,
'comment_url' => (string) $comment->url()
) );
}
/* For QA forums, the comment_type and content_type is an exception */
if ( \IPS\Settings::i()->core_datalayer_distinguish_qa AND
$this instanceof \IPS\forums\Topic AND
$this->container()->_forum_type === 'qa' )
{
$dataLayer['content_type'] = 'question';
if ( isset( $comment ) )
{
$dataLayer['comment_type'] = 'answer';
}
}
/* If we didn't find a Comment or Content type, try pulling that info from the static title fields */
elseif ( ( !isset( $dataLayer['content_type'] ) AND isset( static::$title ) ) OR ( !isset( $dataLayer['comment_type'] ) AND isset( $comment, $comment::$title ) ) )
{
$contentType = ( $app ? preg_replace( "/[{$app}_]?(.*)/", '$1', static::$title ?? "" ) : ( static::$title ?? null ) ) ?: null;
if ( $contentType AND !isset( $dataLayer['content_type'] ) )
{
$dataLayer['content_type'] = $contentType;
}
if ( $comment AND isset( $comment::$title ) AND !isset( $dataLayer['comment_type'] ) )
{
$commentType = ($app or $contentType) ? preg_replace( "/" . ( $app ? "[{$app}_]?" : '' ) . ( $contentType ? "[{$contentType}_]?" : '' ) . "(.*)/", "$1", $comment::$title ) : $comment::$title;
$dataLayer['comment_type'] = $commentType;
}
}
/* Use either comment or review if there still is no comment type (note this should never happen as of IPS v4.6 unless 3rd party things are going on) */
if ( $comment AND !isset( $dataLayer['comment_type'] ) )
{
$dataLayer['comment_type'] = $comment instanceof \IPS\Content\Review ? 'review' : 'comment';
}
try
{
$container = $this->container();
$dataLayer = array_replace( $dataLayer, $container->getDataLayerProperties() );
}
catch ( \OutOfRangeException | \BadMethodCallException $e ) {}
if ( !isset( $dataLayer['content_area'] ) AND $app )
{
$lang = \IPS\Lang::load( \IPS\Lang::defaultLanguage() );
if ( $lang->checkKeyExists( '__app_' . static::$application ) )
{
$dataLayer['content_area'] = $lang->addToStack( '__app_' . static::$application );
}
}
$this->_dataLayerProperties[$index] = \IPS\core\DataLayer::i()->filterProperties( $dataLayer );
}
return $this->_dataLayerProperties[$index];
}
/**
* Add the comment form elements
@@ -4760,16 +4951,7 @@ abstract class _Item extends \IPS\Content
$hidden = NULL;
}
$comment = $commentClass::create( $this, $values[ static::$formLangPrefix . 'comment' . '_' . $this->$idColumn ], FALSE, isset( $values['guest_name'] ) ? $values['guest_name'] : NULL, NULL, NULL, NULL, NULL, $hidden );
$parameters = array_merge( array( 'reply-' . static::$application . '/' . static::$module . '-' . $this->$idColumn ), $comment->attachmentIds() );
\IPS\File::claimAttachments( ...$parameters );
/* Post anonymously */
if( isset( $values[ 'post_anonymously' ] ) )
{
$comment->setAnonymous( $values[ 'post_anonymously' ] );
}
$comment = $commentClass::create( $this, $values[ static::$formLangPrefix . 'comment' . '_' . $this->$idColumn ], FALSE, isset( $values['guest_name'] ) ? $values['guest_name'] : NULL, NULL, NULL, NULL, NULL, $hidden, ( isset( $values[ 'post_anonymously' ] ) ? (bool) $values[ 'post_anonymously' ] : NULL ) );
/* Auto-follow - If posted anonymously we should set follow to anonymous as well */
if( isset( $values[ static::$formLangPrefix . 'auto_follow' ] ) )
@@ -5606,7 +5788,7 @@ abstract class _Item extends \IPS\Content
if ( isset( static::$databaseColumnMap['last_comment_anon'] ) )
{
$lastCommentAnonField = static::$databaseColumnMap['last_comment_anon'];
$this->$lastCommentAnonField = $comment->isAnonymous();
$this->$lastCommentAnonField = (int) $comment->isAnonymous();
}
}
catch ( \UnderflowException $e )
@@ -5634,6 +5816,11 @@ abstract class _Item extends \IPS\Content
$field = static::$databaseColumnMap[$c];
$this->$field = 0;
}
else if( $c === 'last_comment_anon' AND isset( static::$databaseColumnMap['last_comment_anon'] ) )
{
$field = static::$databaseColumnMap[$c];
$this->$field = 0;
}
else
{
if ( isset( static::$databaseColumnMap[$c] ) )
@@ -6310,7 +6497,7 @@ abstract class _Item extends \IPS\Content
{
$member = $member ?: \IPS\Member::loggedIn();
/* If the member is banned they can't do antyhing? */
/* If the member is banned they can't do anything? */
if ( ! ( $member instanceof \IPS\Member\Group ) and ! $member->group['g_view_board'] )
{
return FALSE;
@@ -7771,11 +7958,11 @@ abstract class _Item extends \IPS\Content
}
}
return ( isset ( $this->tags['tags'] ) ? $this->tags['tags'] : NULL);
return ( isset ( $this->tags['tags'] ) ? $this->tags['tags'] : [] );
}
else
{
return NULL;
return [];
}
}
@@ -8755,7 +8942,7 @@ abstract class _Item extends \IPS\Content
'meta_item_id' => $this->$idColumn,
'meta_type' => $type,
'meta_data' => $data,
'meta_item_author' => $this->author()->member_id,
'meta_item_author' => (int) $this->author()->member_id,
'meta_added' => time()
) );
@@ -9212,4 +9399,33 @@ abstract class _Item extends \IPS\Content
{
return array( );
}
/**
* Webhook filters
*
* @return array
*/
public function webhookFilters()
{
$filters = parent::webhookFilters();
if ( \in_array( 'IPS\Content\Lockable', class_implements( $this ) ) )
{
$filters['locked'] = $this->locked();
}
if ( \in_array( 'IPS\Content\Pinnable', class_implements( $this ) ) )
{
$filters['pinned'] = (bool) $this->mapped('pinned');
}
if ( \in_array( 'IPS\Content\Featurable', class_implements( $this ) ) )
{
$filters['featured'] = (bool) $this->mapped('featured');
}
if ( \in_array( 'IPS\Content\Polls', class_implements( $this ) ) )
{
$filters['hasPoll'] = (bool) $this->mapped('poll');
}
return $filters;
}
}
+13 -3
View File
@@ -96,6 +96,11 @@ class _Facebook extends PromoteAbstract
{
$facebook = static::getLoginHandler();
if ( $facebook === NULL )
{
return FALSE;
}
$authorizedScopes = $facebook->authorizedScopes( $this->member );
if( $authorizedScopes === NULL )
@@ -165,7 +170,7 @@ class _Facebook extends PromoteAbstract
{
foreach( $this->settings['shareable']['pages'] as $id => $data )
{
$options[ 'p_' . $id ] = \IPS\Member::loggedIn()->language()->addToStack( 'promote_facebook_page_prefix', NULL, array( 'sprintf' => array( $data['name'] ) ) );
$options[ 'p_' . $id ] = \IPS\Member::loggedIn()->language()->addToStack( 'promote_facebook_page_prefix', FALSE, array( 'sprintf' => array( $data['name'] ) ) );
$names[] = $data['name'];
}
}
@@ -174,7 +179,7 @@ class _Facebook extends PromoteAbstract
{
foreach( $this->settings['shareable']['groups'] as $id => $data )
{
$options[ 'g_' . $id ] = \IPS\Member::loggedIn()->language()->addToStack( 'promote_facebook_group_prefix', NULL, array( 'sprintf' => array( $data['name'] ) ) );
$options[ 'g_' . $id ] = \IPS\Member::loggedIn()->language()->addToStack( 'promote_facebook_group_prefix', FALSE, array( 'sprintf' => array( $data['name'] ) ) );
$names[] = $data['name'];
}
}
@@ -277,7 +282,7 @@ class _Facebook extends PromoteAbstract
\IPS\Log::log( $e, 'facebook' );
}
if ( ! $photos or \count( $photos ) === 1 )
if ( ! $photos or ( \is_array( $photos ) and \count( $photos ) === 1 ) )
{
/* Simple message */
try
@@ -450,6 +455,11 @@ class _Facebook extends PromoteAbstract
{
$facebook = \IPS\Login\Handler::findMethod('IPS\Login\Handler\Oauth2\Facebook');
if ( $facebook === NULL )
{
return NULL;
}
/* We are using the default app here */
if ( ! \is_numeric( \IPS\Settings::i()->promote_facebook_auth ) )
{
+7 -2
View File
@@ -106,6 +106,11 @@ class _Twitter extends PromoteAbstract
public function canPostToPage()
{
$twitter = \IPS\Login\Handler::findMethod('IPS\Login\Handler\Oauth1\Twitter');
if ( $twitter === NULL )
{
return FALSE;
}
return $twitter->hasWritePermissions( $this->settings['token'], $this->settings['secret'] );
}
@@ -194,8 +199,8 @@ class _Twitter extends PromoteAbstract
{
$photos = $promote->imageObjects();
$mediaIds = array();
if ( \count( $photos ) )
if ( \is_array( $photos ) and \count( $photos ) )
{
try
{
+18 -2
View File
@@ -152,6 +152,22 @@ trait Reactable
$this->_reactions = NULL;
$this->hasReacted[ $member->member_id ] = $reaction;
if( \IPS\Application::appIsEnabled( 'cloud' ) and \IPS\cloud\Realtime::i()->isEnabled('realtime') )
{
$this->reactions();
\IPS\cloud\Realtime::i()->publishEvent( 'reaction-count', array('count' => $this->_reactionCount) );
}
if( \IPS\Application::appIsEnabled( 'cloud' ) and \IPS\cloud\Realtime::i()->isEnabled('trending') )
{
/* We need to make sure we're using the item */
$item = ( ! $this instanceof \IPS\Content\Item ) ? $this->item() : $this;
$itemIdColumn = $item::$databaseColumnId;
$itemId = $item->$itemIdColumn;
\IPS\Redis::i()->zIncrBy( 'trending', time() * 0.4, \get_class( $item ) .'__' . $itemId );
}
}
/**
@@ -426,9 +442,9 @@ trait Reactable
try
{
$class = $row['rep_class'];
$comment = $class::load( $row['type_id'] );
$item = $class::load( $row['type_id'] );
return \IPS\Member::loggedIn()->language()->addToStack( '_defart_from_date', NULL, array( 'htmlsprintf' => array( $comment->url(), $comment::$title, \IPS\DateTime::ts( $date )->html() ) ) );
return \IPS\Member::loggedIn()->language()->addToStack( '_defart_from_date', FALSE, array( 'htmlsprintf' => array( $item->url(), $item->mapped('title'), \IPS\DateTime::ts( $date )->html() ) ) );
}
catch( \Exception $e )
{
+13 -7
View File
@@ -72,6 +72,12 @@ trait Recognizable
*/
public function canRecognize( ?\IPS\Member $member=NULL ): bool
{
/* Are achievements enabled? */
if ( !\IPS\Settings::i()->achievements_enabled )
{
return FALSE;
}
/* Already recognized? */
if ( isset( $this->recognized ) )
{
@@ -161,38 +167,38 @@ trait Recognizable
public function recognizedBlurb(): array
{
$return = [ 'message' => NULL ];
$return['main'] = \IPS\Member::loggedIn()->language()->addToStack('recognize_blurb_main', NULL, [ 'htmlsprintf' => [ $this::$title, $this->recognized->_given_by->link() ] ] );
$return['main'] = \IPS\Member::loggedIn()->language()->addToStack( 'recognize_blurb_main', FALSE, [ 'htmlsprintf' => [ $this::$title, $this->recognized->_given_by->link() ] ] );
if ( $this->recognized->badge and $this->recognized->points )
{
if ( $this->author()->member_id === \IPS\Member::loggedIn()->member_id )
{
$return['awards'] = \IPS\Member::loggedIn()->language()->addToStack( 'recognize_blurb_awarded_points_and_badge', NULL, ['htmlsprintf' => [$this->recognized->badge()->_title, $this->recognized->points]] );
$return['awards'] = \IPS\Member::loggedIn()->language()->addToStack( 'recognize_blurb_awarded_points_and_badge', FALSE, ['htmlsprintf' => [$this->recognized->badge()->_title, $this->recognized->points]] );
}
else
{
$return['awards'] = \IPS\Member::loggedIn()->language()->addToStack( 'recognize_blurb_awarded_points_and_badge_third', NULL, ['htmlsprintf' => [$this->author()->name, $this->recognized->badge()->_title, $this->recognized->points]] );
$return['awards'] = \IPS\Member::loggedIn()->language()->addToStack( 'recognize_blurb_awarded_points_and_badge_third', FALSE, ['htmlsprintf' => [$this->author()->name, $this->recognized->badge()->_title, $this->recognized->points]] );
}
}
else if ( $this->recognized->points )
{
if ( $this->author()->member_id === \IPS\Member::loggedIn()->member_id )
{
$return['awards'] = \IPS\Member::loggedIn()->language()->addToStack('recognize_blurb_awarded_points', NULL, [ 'sprintf' => [ $this->recognized->points ] ] );
$return['awards'] = \IPS\Member::loggedIn()->language()->addToStack( 'recognize_blurb_awarded_points', FALSE, [ 'sprintf' => [ $this->recognized->points ] ] );
}
else
{
$return['awards'] = \IPS\Member::loggedIn()->language()->addToStack('recognize_blurb_awarded_points_third', NULL, [ 'sprintf' => [ $this->author()->name, $this->recognized->points ] ] );
$return['awards'] = \IPS\Member::loggedIn()->language()->addToStack( 'recognize_blurb_awarded_points_third', FALSE, [ 'sprintf' => [ $this->author()->name, $this->recognized->points ] ] );
}
}
else if ( $this->recognized->badge )
{
if ( $this->author()->member_id === \IPS\Member::loggedIn()->member_id )
{
$return['awards'] = \IPS\Member::loggedIn()->language()->addToStack('recognize_blurb_awarded_badge', NULL, [ 'htmlsprintf' => [ $this->recognized->badge()->_title ] ] );
$return['awards'] = \IPS\Member::loggedIn()->language()->addToStack( 'recognize_blurb_awarded_badge', FALSE, [ 'htmlsprintf' => [ $this->recognized->badge()->_title ] ] );
}
else
{
$return['awards'] = \IPS\Member::loggedIn()->language()->addToStack('recognize_blurb_awarded_badge_third', NULL, [ 'htmlsprintf' => [ $this->author()->name, $this->recognized->badge()->_title ] ] );
$return['awards'] = \IPS\Member::loggedIn()->language()->addToStack( 'recognize_blurb_awarded_badge_third', FALSE, [ 'htmlsprintf' => [ $this->author()->name, $this->recognized->badge()->_title ] ] );
}
}
+2 -1
View File
@@ -44,10 +44,11 @@ abstract class _Review extends \IPS\Content\Comment
* @param \IPS\DateTime|NULL $time The time
* @param string|NULL $ipAddress The IP address or NULL to detect automatically
* @param int|NULL $hiddenStatus NULL to set automatically or override: 0 = unhidden; 1 = hidden, pending moderator approval; -1 = hidden (as if hidden by a moderator)
* @param int|NULL $anonymous NULL for no value, 0 or 1 for a value (0=no, 1=yes)
* @return static
* @throws \InvalidArgumentException
*/
public static function create( $item, $comment, $first=FALSE, $rating=NULL, $guestName=NULL, $member=NULL, \IPS\DateTime $time=NULL, $ipAddress=NULL, $hiddenStatus=NULL )
public static function create( $item, $comment, $first=FALSE, $rating=NULL, $guestName=NULL, $member=NULL, \IPS\DateTime $time=NULL, $ipAddress=NULL, $hiddenStatus=NULL, $anonymous=NULL )
{
if ( !\is_int( $rating ) or $rating < 1 or $rating > \intval( \IPS\Settings::i()->reviews_rating_out_of ) )
{
+71 -57
View File
@@ -25,8 +25,8 @@ class _Index extends \IPS\Content\Search\Index
/**
* @brief Elasticsearch version requirements
*/
const MINIMUM_VERSION = '6.0.0';
const UNSUPPORTED_VERSION = '7.7.0';
const MINIMUM_VERSION = '7.2.0';
const UNSUPPORTED_VERSION = '8.0.0';
/**
* @brief The server URL
@@ -61,7 +61,7 @@ class _Index extends \IPS\Content\Search\Index
$analyzer = key( $settings['analysis']['analyzer'] );
}
$this->url->request()->delete();
\IPS\Content\Search\Elastic\Index::request( $this->url )->delete();
$definition = array(
'settings' => $settings,
@@ -106,6 +106,8 @@ class _Index extends \IPS\Content\Search\Index
'index_comments' => array( 'type' => 'long' ),
'index_reviews' => array( 'type' => 'long' ),
'index_participants' => array( 'type' => 'long' ),
'index_is_anon' => array( 'type' => 'byte' ),
'index_item_solved' => array( 'type' => 'byte' )
)
)
)
@@ -113,7 +115,7 @@ class _Index extends \IPS\Content\Search\Index
try
{
$response = $this->url->setQueryString( 'include_type_name', 'true' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->put( json_encode( $definition ) );
$response = \IPS\Content\Search\Elastic\Index::request( $this->url->setQueryString( 'include_type_name', 'true' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->put( json_encode( $definition ) );
if( $response->httpResponseCode != 200 )
{
@@ -122,7 +124,7 @@ class _Index extends \IPS\Content\Search\Index
}
catch( \Exception $e )
{
$response = $this->url->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->put( json_encode( $definition ) );
$response = \IPS\Content\Search\Elastic\Index::request( $this->url )->setHeaders( array( 'Content-Type' => 'application/json' ) )->put( json_encode( $definition ) );
}
}
@@ -137,16 +139,15 @@ class _Index extends \IPS\Content\Search\Index
if ( $indexData = parent::indexData( $object ) )
{
$indexData['index_permissions'] = explode( ',', $indexData['index_permissions'] );
$indexData['index_tags'] = $indexData['index_tags'] ? explode( ',', $indexData['index_tags'] ) : array();
$indexData['index_is_last_comment'] = (bool) $indexData['index_is_last_comment'];
if ( $object instanceof \IPS\Content\Item )
{
$indexData += $this->metaCounts( $object );
$indexData = array_merge( $indexData, $this->metaData( $object ) );
}
else
{
$indexData += $this->metaCounts( $object->item() );
$indexData = array_merge( $indexData, $this->metaData( $object->item() ) );
}
return $indexData;
@@ -181,7 +182,7 @@ class _Index extends \IPS\Content\Search\Index
try
{
$r = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/' . $this->getIndexId( $object ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get()->decodeJson();
$r = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/' . $this->getIndexId( $object ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get()->decodeJson();
if ( $r['found'] )
{
$existingData = $r['_source'];
@@ -204,9 +205,7 @@ class _Index extends \IPS\Content\Search\Index
}
/* Insert into index */
$r = $this->url
->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/' . $this->getIndexId( $object ) )
->request( \IPS\LONG_REQUEST_TIMEOUT )
$r = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/' . $this->getIndexId( $object ) ), \IPS\LONG_REQUEST_TIMEOUT )
->setHeaders( array( 'Content-Type' => 'application/json' ) )
->put( json_encode( $indexData ) );
@@ -215,6 +214,11 @@ class _Index extends \IPS\Content\Search\Index
throw new \IPS\Content\Search\Elastic\Exception( $error['type'] . ': ' . $error['reason'] );
}
if ( $resetLastComment )
{
$this->resetLastComment( array( $indexData['index_class'] ), $indexData['index_item_id'] );
}
/* Views / Comments / Reviews */
if ( $object instanceof \IPS\Content\Item )
{
@@ -224,7 +228,8 @@ class _Index extends \IPS\Content\Search\Index
{
$item = $object->item();
}
$this->rebuildMetaCounts( $item, $indexData['index_is_last_comment'] ? $this->getIndexId( $object ) : NULL );
$this->rebuildMetaData( $item );
}
catch ( \IPS\Http\Request\Exception $e )
{
@@ -234,11 +239,6 @@ class _Index extends \IPS\Content\Search\Index
{
\IPS\Log::log( $e, 'elasticsearch_response_error' );
}
if ( $resetLastComment )
{
$this->resetLastComment( array( $indexData['index_class'] ), $indexData['index_item_id'] );
}
}
}
}
@@ -253,7 +253,7 @@ class _Index extends \IPS\Content\Search\Index
try
{
/* This request *intentionally* goes to _tasks and not (ourpath)/_tasks */
$response = $this->url->setPath( '/_tasks' )->request( \IPS\LONG_REQUEST_TIMEOUT )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get();
$response = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( '/_tasks' ), \IPS\LONG_REQUEST_TIMEOUT )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get();
if( $error = $this->getResponseError( $response ) )
{
@@ -278,7 +278,7 @@ class _Index extends \IPS\Content\Search\Index
continue;
}
$this->url->setPath( '/.tasks/task' . $taskId )->request( \IPS\LONG_REQUEST_TIMEOUT )->setHeaders( array( 'Content-Type' => 'application/json' ) )->delete();
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( '/.tasks/task' . $taskId ), \IPS\LONG_REQUEST_TIMEOUT )->setHeaders( array( 'Content-Type' => 'application/json' ) )->delete();
}
}
}
@@ -298,7 +298,7 @@ class _Index extends \IPS\Content\Search\Index
* @param \IPS\Content\Searchable $item The content item
* @return void
*/
protected function metaCounts( $item )
protected function metaData( $item )
{
$databaseColumnId = $item::$databaseColumnId;
@@ -318,7 +318,9 @@ class _Index extends \IPS\Content\Search\Index
return array(
'index_comments' => $item->mapped('num_comments'),
'index_reviews' => $item->mapped('num_reviews'),
'index_participants' => $participants
'index_participants' => $participants,
'index_tags' => $item->tags(),
'index_prefix' => $item->prefix(),
);
}
@@ -326,10 +328,9 @@ class _Index extends \IPS\Content\Search\Index
* Rebuild the comment / review counts for an item
*
* @param \IPS\Content\Searchable $item The content item
* @param string $lastCommentIndexId The index ID of the last comment, if you want to reset others
* @return void
*/
protected function rebuildMetaCounts( $item, $lastCommentIndexId = NULL )
protected function rebuildMetaData( $item )
{
$databaseColumnId = $item::$databaseColumnId;
$class = \get_class( $item );
@@ -347,12 +348,12 @@ class _Index extends \IPS\Content\Search\Index
{
$updates = array();
$params = array();
foreach ( $this->metaCounts( $item ) as $k => $v )
foreach ( $this->metaData( $item ) as $k => $v )
{
if ( \is_array( $v ) )
{
$updates[] = "ctx._source.{$k} = params.param_{$k};";
$params[ 'param_' . $k ] = array_map( 'intval', $v );
$params[ 'param_' . $k ] = ( $k !== 'index_tags' ? array_map( 'intval', $v ) : $v );
}
elseif ( \is_null( $v ) )
{
@@ -365,12 +366,8 @@ class _Index extends \IPS\Content\Search\Index
$params['param_' . $k ] = \intval( $v );
}
}
if ( !\is_null( $lastCommentIndexId ) )
{
$updates[] = "ctx._source.index_is_last_comment = ( ctx._id == params.param_lastcommentid );";
$params['param_lastcommentid'] = $lastCommentIndexId;
}
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => \IPS\Settings::i()->search_index_maxresults ) )->request( \IPS\LONG_REQUEST_TIMEOUT )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
$r = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => \IPS\Settings::i()->search_index_maxresults ) ), \IPS\LONG_REQUEST_TIMEOUT )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'script' => array(
'source' => implode( ' ', $updates ),
'lang' => 'painless',
@@ -429,7 +426,7 @@ class _Index extends \IPS\Content\Search\Index
if ( !( $object instanceof \IPS\Content\Item ) )
{
$this->rebuildMetaCounts( $object->item() );
$this->rebuildMetaData( $object->item() );
$itemClass = \get_class( $object->item() );
$itemIdColumn = $itemClass::$databaseColumnId;
@@ -465,14 +462,14 @@ class _Index extends \IPS\Content\Search\Index
try
{
$indexId = \strtolower( str_replace( '\\', '_', \substr( $class, 4 ) ) ) . '-' . $id;
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/' . $indexId )->request()->delete();
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/' . $indexId ) )->delete();
if ( is_subclass_of( $class, 'IPS\Content\Item' ) )
{
if ( isset( $class::$commentClass ) )
{
$commentClass = $class::$commentClass;
$response = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
$response = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'query' => array(
'bool' => array(
'must' => array(
@@ -495,7 +492,7 @@ class _Index extends \IPS\Content\Search\Index
if ( isset( $class::$reviewClass ) )
{
$reviewClass = $class::$reviewClass;
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'query' => array(
'bool' => array(
'must' => array(
@@ -565,15 +562,15 @@ class _Index extends \IPS\Content\Search\Index
'index_author' => $authorId
)
);
}
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
}
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'query' => $query
) ) );
}
else
{
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'query' => array(
'term' => array(
'index_class' => $class
@@ -677,7 +674,7 @@ class _Index extends \IPS\Content\Search\Index
if ( \count( $updates ) )
{
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => \IPS\Settings::i()->search_index_maxresults ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => \IPS\Settings::i()->search_index_maxresults ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'script' => array(
'source' => implode( ' ', $updates ),
'lang' => 'painless',
@@ -697,8 +694,8 @@ class _Index extends \IPS\Content\Search\Index
'index_author' => 0
)
);
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => \IPS\Settings::i()->search_index_maxresults ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => \IPS\Settings::i()->search_index_maxresults ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'script' => array(
'source' => "ctx._source.index_permissions.add( 'm' + ctx._source.index_author );",
'lang' => 'painless'
@@ -727,8 +724,8 @@ class _Index extends \IPS\Content\Search\Index
)
);
}
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => \IPS\Settings::i()->search_index_maxresults ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => \IPS\Settings::i()->search_index_maxresults ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'script' => array(
'source' => "ctx._source.index_hidden = params.newHiddenStatus;",
'lang' => 'painless',
@@ -766,7 +763,7 @@ class _Index extends \IPS\Content\Search\Index
{
return array(
'bool' => array(
'must' => array( $conditions )
'must' => $conditions
)
);
}
@@ -795,7 +792,7 @@ class _Index extends \IPS\Content\Search\Index
try
{
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/' . $this->getIndexId( $item ) . '/_update' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/' . $this->getIndexId( $item ) . '/_update' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'doc' => $update
) ) );
@@ -827,7 +824,7 @@ class _Index extends \IPS\Content\Search\Index
{
try
{
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'query' => array(
'range' => array(
'index_date_updated' => array(
@@ -861,7 +858,7 @@ class _Index extends \IPS\Content\Search\Index
try
{
/* Remove the flag */
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => \IPS\Settings::i()->search_index_maxresults ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
$r = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'script' => array(
'source' => "ctx._source.index_is_last_comment = false;",
'lang' => 'painless'
@@ -921,15 +918,15 @@ class _Index extends \IPS\Content\Search\Index
if ( $lastComment = $item->comments( 1, 0, 'date', 'desc', NULL, FALSE, NULL, $where ) AND $lastComment instanceof \IPS\Content\Searchable )
{
/* Set that it is the latest comment */
$r = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/' . $this->getIndexId( $lastComment ) . '/_update' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
$r = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_update/' . $this->getIndexId( $lastComment ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'doc' => array(
'index_is_last_comment' => true
)
) ) );
/* And set the updated time on the main item (done as _update_by_query because it might not exist if the first comment is required) */
$indexDataForLastComment = $this->indexData( $lastComment );
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => \IPS\Settings::i()->search_index_maxresults ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
$r = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'script' => array(
'source' => "ctx._source.index_date_updated = params.dateUpdated; ctx._source.index_date_commented = params.dateCommented;",
'lang' => 'painless',
@@ -1001,7 +998,7 @@ class _Index extends \IPS\Content\Search\Index
);
/* Get the count */
$count = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_search' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
$count = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_search' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
'size' => 0,
'query' => $query
) ) )->decodeJson();
@@ -1012,7 +1009,7 @@ class _Index extends \IPS\Content\Search\Index
}
/* Now get the unique item ids */
$results = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_search' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
$results = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_search' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
'aggs' => array(
'itemIds' => array(
'terms' => array(
@@ -1054,7 +1051,7 @@ class _Index extends \IPS\Content\Search\Index
{
try
{
$results = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_search' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
$results = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_search' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
'query' => array(
'bool' => array(
'filter' => array(
@@ -1103,7 +1100,7 @@ class _Index extends \IPS\Content\Search\Index
{
try
{
$results = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_search' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
$results = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_search' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
'size' => 1,
'sort' => array( array( 'index_date_updated' => 'asc' ) )
) ) )->decodeJson();
@@ -1139,7 +1136,7 @@ class _Index extends \IPS\Content\Search\Index
try
{
$results = $this->url->setPath( '/_analyze' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
$results = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( '/_analyze' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
'analyzer' => $analyzer,
'text' => implode( ' ', $terms )
) ) )->decodeJson();
@@ -1172,6 +1169,23 @@ class _Index extends \IPS\Content\Search\Index
return FALSE;
}
/**
* Wrapper to account for log in where needed
*
* @param $url
* @param null $timeout
* @return \IPS\Http\Request\Curl|Socket
*/
public static function request( $url, $timeout=NULL )
{
if ( \IPS\ELASTICSEARCH_USER or \IPS\ELASTICSEARCH_PASSWORD )
{
return $url->request( $timeout )->login( \IPS\ELASTICSEARCH_USER, \IPS\ELASTICSEARCH_PASSWORD );
}
return $url->request( $timeout );
}
/**
* Check response to see if an error was produced
*
@@ -73,7 +73,7 @@ class _MassIndexer extends \IPS\Content\Search\Elastic\Index
}
$json = implode( "\n", $json );
$this->url->setPath( '/_bulk' )->request()->setHeaders( array( 'Content-Type' => 'application/x-ndjson' ) )->post( $json . "\n" );
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( '/_bulk' ) )->setHeaders( array( 'Content-Type' => 'application/x-ndjson' ) )->post( $json . "\n" );
}
}
}
+173 -15
View File
@@ -69,7 +69,12 @@ class _Query extends \IPS\Content\Search\Query
parent::__construct( $member );
$this->url = $url;
}
/**
* @var bool Stores whether or not any content type needs to be limited by the latest comment
*/
protected $lastCommentMustBeTrue = false;
/**
* Filter by multiple content types
*
@@ -198,11 +203,14 @@ class _Query extends \IPS\Content\Search\Query
/* Only last comment? */
if ( $filter->onlyLastComment )
{
/* Set this for when the search JSON is built */
$this->lastCommentMustBeTrue = true;
$conditions[] = array(
'term' => array( 'index_is_last_comment' => true )
);
}
/* Put it together */
if( \count( $conditions ) )
{
@@ -215,7 +223,7 @@ class _Query extends \IPS\Content\Search\Query
{
$this->filters[] = array(
'bool' => array(
( $type ? 'should' : 'must_not' ) => array( $contentFilterConditions )
( $type ? 'should' : 'must_not' ) => $contentFilterConditions
)
);
}
@@ -363,7 +371,79 @@ class _Query extends \IPS\Content\Search\Query
/* Return for daisy chaining */
return $this;
}
/**
* Stores the "more like this" Content object for search()
* @param null
*/
protected $moreLikeThis = NULL;
/**
* Filter by more like this
* @param \IPS\Content\Searchable $object
*/
public function filterByMoreLikeThis( \IPS\Content\Searchable $object )
{
$index = new \IPS\Content\Search\Elastic\Index( $this->url );
$this->moreLikeThis = $index->getIndexId( ( $object instanceof \IPS\Content\Item and $object::$firstCommentRequired ) ? $object->firstComment() : $object );
/* Some container types cannot be cached */
$classes = [];
$conditions = [];
foreach ( \IPS\Application::allExtensions( 'core', 'ContentRouter', FALSE ) as $object )
{
$classes = array_merge( $object->classes, $classes );
}
/* Some nodes have complex permissions, so avoid any permission issues by not selecting these */
foreach( $classes as $itemClass )
{
if ( isset( $itemClass::$containerNodeClass ) )
{
$containerClass = $itemClass::$containerNodeClass;
$blockIds = [];
if ( $customNodes = $containerClass::customPermissionNodes() )
{
foreach ( $customNodes as $key => $ids )
{
if ( $key !== 'count' )
{
$blockIds = array_merge( $blockIds, $ids );
}
}
}
if ( \count( $blockIds ) )
{
$conditions[] = array(
'bool' => array(
'filter' => array(
array(
'terms' => array( 'index_container_class' => [ $containerClass ] )
),
array(
'terms' => array( 'index_container_id' => $blockIds )
),
)
)
);
}
if ( \count( $conditions ) )
{
foreach( $conditions as $condition )
{
$this->mustNot[] = $condition;
}
}
}
}
/* Only show non-hidden and approved items */
$this->setHiddenFilter( static::HIDDEN_VISIBLE );
}
/**
* Filter by container class
*
@@ -558,9 +638,7 @@ class _Query extends \IPS\Content\Search\Query
if ( $includeMembers and $followed = iterator_to_array( \IPS\Db::i()->select( 'follow_rel_id', 'core_follow', array( 'follow_app=? AND follow_area=? AND follow_member_id=?', 'core', 'member', $this->member->member_id ), 'follow_rel_id asc' ) ) )
{
$conditions[] = array(
array(
'terms' => array( 'index_author' => $followed )
)
'terms' => array( 'index_author' => $followed )
);
}
@@ -799,7 +877,35 @@ class _Query extends \IPS\Content\Search\Query
return $this;
}
/**
* Filter by solved
*
* @return \IPS\Content\Search\Query (for daisy chaining)
*/
public function filterBySolved()
{
$this->filters[] = array(
'term' => array( 'index_item_solved' => 1 )
);
return $this;
}
/**
* Filter by Unsolved
*
* @return \IPS\Content\Search\Query (for daisy chaining)
*/
public function filterByUnsolved()
{
$this->filters[] = array(
'term' => array( 'index_item_solved' => 0 )
);
return $this;
}
/**
* Filter by start date
*
@@ -922,7 +1028,35 @@ class _Query extends \IPS\Content\Search\Query
return $this;
}
/**
* Debug by itemId, used for debugging purposes only
* @note No permission checks run, do not use in production
* @param $itemId
* @return void
*/
public function debugByItemId( $itemId )
{
$array = array(
'query' => [
'term' => [
'index_item_id' => [
'value' => \intval( $itemId )
]
]
],
'sort' => $this->sort ?: array(),
'from' => 0,
'size' => 50,
);
$json = json_encode( $array, JSON_PARTIAL_OUTPUT_ON_ERROR );
return \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_search' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( $json )->decodeJson();
}
/**
* Search
*
@@ -1017,6 +1151,12 @@ class _Query extends \IPS\Content\Search\Query
)
)
);
/* If we're not searching with a term, then just show the title record, not comments */
if ( $term === NULL and ! $this->lastCommentMustBeTrue )
{
$must[] = ['exists' => ['field' => 'index_title']];
}
}
/* Put that with the rest of the conditions */
@@ -1050,7 +1190,25 @@ class _Query extends \IPS\Content\Search\Query
$filters[] = array( 'term' => array( 'index_hidden' => $this->hiddenStatuses ) );
}
}
$searchUrl = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_search' );
if ( $this->moreLikeThis )
{
$must[] = [
'more_like_this' => [
'fields' => [ 'index_title' ],
'like' => [ '_index' => \IPS\Settings::i()->search_elastic_index, '_id' => $this->moreLikeThis ],
'min_term_freq' => 1,
'max_query_terms' => 12,
'min_word_length' => 3,
]
];
/* Let's enforce a relatively short timeout to prevent slow ES server from dragging down topics */
$searchUrl = $searchUrl->setQueryString( 'timeout', '3s' );
}
/* Peform the search */
try
{
@@ -1062,9 +1220,9 @@ class _Query extends \IPS\Content\Search\Query
'filter' => $filters
)
);
/* Add the time decay */
if ( \IPS\Settings::i()->search_decay_factor and !$this->sort )
if ( \IPS\Settings::i()->search_decay_factor and !$this->sort and ! $this->sort )
{
$query = array(
'function_score' => array(
@@ -1081,7 +1239,7 @@ class _Query extends \IPS\Content\Search\Query
}
/* Add the self boost */
if ( \IPS\Settings::i()->search_elastic_self_boost and $this->member->member_id and !$this->sort )
if ( \IPS\Settings::i()->search_elastic_self_boost and $this->member->member_id and !$this->sort and ! $this->moreLikeThis )
{
$query = array(
'function_score' => array(
@@ -1117,10 +1275,10 @@ class _Query extends \IPS\Content\Search\Query
}
/* Make the call! */
$return = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_search' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( $json )->decodeJson();
$return = \IPS\Content\Search\Elastic\Index::request( $searchUrl )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( $json )->decodeJson();
if ( isset( $return['error'] ) )
{
\IPS\Log::log( print_r( $return['error'], TRUE ), 'elasticsearch' );
\IPS\Log::log( print_r( array_merge( $array, ['error' => $return['error'] ] ), TRUE ), 'elasticsearch' );
return new \IPS\Content\Search\Results( array(), 0 );
}
+12 -1
View File
@@ -309,6 +309,7 @@ abstract class _Index extends \IPS\Patterns\Singleton
/* Get the item index ID */
$itemIndexId = NULL;
$itemClass = NULL;
if ( $object instanceof \IPS\Content\Comment )
{
$itemClass = $object::$itemClass;
@@ -390,6 +391,15 @@ abstract class _Index extends \IPS\Patterns\Singleton
$containerClass = ( $object->item()->searchIndexContainerClass() ) ? \get_class( $object->item()->searchIndexContainerClass() ) : NULL;
}
/* Do we have an extension to modify this? */
foreach( \IPS\Application::enabledApplications() as $app )
{
foreach ( $app->extensions( 'core', 'SearchIndex' ) as $extension )
{
$content = $extension->content( $object, $content );
}
}
/* Return */
return array(
'index_class' => $class,
@@ -412,7 +422,8 @@ abstract class _Index extends \IPS\Patterns\Singleton
'index_is_last_comment' => $isLastComment,
'index_club_id' => $clubId,
'index_class_type_id_hash' => md5( $class . ':' . $object->$idColumn ),
'index_is_anon' => $object->isAnonymous()
'index_is_anon' => (int) $object->isAnonymous(),
'index_item_solved' => ( $itemClass and \IPS\IPS::classUsesTrait( $itemClass, 'IPS\Content\Solvable' ) ) ? ( $object->item()->mapped('solved_comment_id') == $object->$idColumn ) ? 1 : 0 : NULL
);
}
+73 -65
View File
@@ -609,7 +609,31 @@ class _Query extends \IPS\Content\Search\Query
$this->filterByUnread = TRUE;
}
/**
* Filter only solved content
*
* @return \IPS\Content\Search\Query (for daisy chaining)
*/
public function filterBySolved()
{
$this->where[] = array( 'index_item_solved=?', 1 );
return $this;
}
/**
* Filter only unsolved content
*
* @return \IPS\Content\Search\Query (for daisy chaining)
*/
public function filterByUnsolved()
{
$this->where[] = array( 'index_item_solved=?', 0 );
return $this;
}
/**
* Get the 'unread' where SQL
*
@@ -629,54 +653,61 @@ class _Query extends \IPS\Content\Search\Query
$unreadWheres = array();
$containerIds = array();
$containerClass = isset( $class::$containerNodeClass ) ? $class::$containerNodeClass : NULL;
foreach( $resetTimes as $containerId => $timestamp )
{
/* Pages has different classes per database, but recorded as 'cms' and the container ID in the marking tables */
if ( $containerClass and method_exists( $containerClass, 'isValidContainerId' ) )
{
if ( ! $containerClass::isValidContainerId( $containerId ) )
{
continue;
}
}
$timestamp = $timestamp ?: $this->member->marked_site_read;
$containerIds[] = $containerId;
$unreadWheres[] = '( index_container_id=' . $containerId . ' AND index_date_updated > ' . (int) $timestamp . ')';
$items = $this->member->markersItems( $application, \IPS\Content\Item::makeMarkerKey( $containerId ) );
if ( \count( $items ) )
if ( \is_array( $resetTimes ) )
{
foreach( $resetTimes as $containerId => $timestamp )
{
foreach( $items as $mid => $mtime )
/* Pages has different classes per database, but recorded as 'cms' and the container ID in the marking tables */
if ( $containerClass and method_exists( $containerClass, 'isValidContainerId' ) )
{
if ( $mtime > $timestamp )
if ( ! $containerClass::isValidContainerId( $containerId ) )
{
/* If an item has been moved from one container to another, the user may have a marker
in it's old location, with the previously 'read' time. In this circumstance, we need
to only use more recent read time, otherwise the topic may be incorrectly included
in the results */
if ( \in_array( $mid, $markers ) )
continue;
}
}
$timestamp = $timestamp ?: $this->member->marked_site_read;
$containerIds[] = $containerId;
$unreadWheres[] = '( index_container_id=' . $containerId . ' AND index_date_updated > ' . (int) $timestamp . ')';
$items = $this->member->markersItems( $application, \IPS\Content\Item::makeMarkerKey( $containerId ) );
if ( \count( $items ) )
{
foreach( $items as $mid => $mtime )
{
if ( $mtime > $timestamp )
{
$_key = array_search( $mid, $markers );
$_mtime = \intval( mb_substr( $_key, 0, mb_strpos( $_key, '.' ) ) );
if ( $_mtime < $mtime )
/* If an item has been moved from one container to another, the user may have a marker
in it's old location, with the previously 'read' time. In this circumstance, we need
to only use more recent read time, otherwise the topic may be incorrectly included
in the results */
if ( \in_array( $mid, $markers ) )
{
unset( $markers[ $_key ] );
}
/* If the existing timestamp is higher, retain that since we reset the $markers array below */
else
{
$mtime = $_mtime;
$_key = array_search( $mid, $markers );
$_mtime = \intval( mb_substr( $_key, 0, mb_strpos( $_key, '.' ) ) );
if ( $_mtime < $mtime )
{
unset( $markers[ $_key ] );
}
/* If the existing timestamp is higher, retain that since we reset the $markers array below */
else
{
$mtime = $_mtime;
}
}
$markers[ $mtime . '.' . $mid ] = $mid;
}
$markers[ $mtime . '.' . $mid ] = $mid;
}
}
}
}
else
{
$unreadWheres[] = "( index_date_updated > " . \intval( $resetTimes ) . ")";
}
if( \count( $containerIds ) )
@@ -972,37 +1003,13 @@ class _Query extends \IPS\Content\Search\Query
$results = iterator_to_array( \IPS\Db::i()->select( 'index_item_id, index_class', array( 'core_search_index_item_map', 'sub' ), array( array( 'index_author_id=' . \intval( $this->member->member_id ) . ' AND ' . \IPS\Db::i()->in( 'index_class', $classes ) ) ), 'index_item_id desc', array( 0, 501 ) ) );
if ( \count( $results ) == 501 )
if ( \count( $results ) )
{
/* Assume we have loads of matches, so do a full query */
$subClause = array( array( 'sub.index_item_id=main.index_item_id AND index_author_id=' . \intval( $this->member->member_id ) . ' AND ' . $this->makeEmbeddedIfClauseForSearchMapTable('sub.index_class') ) );
$query = \IPS\Db::i()->select( 'index_item_id', array( 'core_search_index_item_map', 'sub' ), $subClause );
$where[] = array( 'EXISTS(?)', $query );
}
else if ( \count( $results ) )
{
$classIds = array();
foreach( $results as $result )
{
$classIds[ $result['index_class'] ][] = $result['index_item_id'];
}
$subWhere = array();
foreach( $classIds as $class => $ids )
{
$stack = array( $class );
if ( isset( $class::$commentClass ) )
{
$stack[] = $class::$commentClass;
}
$subWhere[] = '(' . \IPS\Db::i()->in( 'index_class', $stack ) . ' and ' . \IPS\Db::i()->in( 'index_item_id', $ids ) . ')';
}
/* IN is not a SIN. It's been a long day */
$where[] = '(' . implode( ' OR ', $subWhere ) . ')';
}
else
{
/* There are no results */
@@ -1260,8 +1267,9 @@ class _Query extends \IPS\Content\Search\Query
{
$order .= ", index_date_updated";
}
if( $this->filterByUnread === TRUE )
/* 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';
}
+14
View File
@@ -174,6 +174,20 @@ abstract class _Query
* @return \IPS\Content\Search\Query (for daisy chaining)
*/
abstract public function filterByUnread();
/**
* Filter by content the user has not read
*
* @return \IPS\Content\Search\Query (for daisy chaining)
*/
abstract public function filterBySolved();
/**
* Filter by content the user has not read
*
* @return \IPS\Content\Search\Query (for daisy chaining)
*/
abstract public function filterByUnsolved();
/**
* Filter by start date
+1 -1
View File
@@ -138,7 +138,7 @@ class _Content extends \IPS\Content\Search\Result
catch( \Exception $ex ) { }
}
$document->addItem( $object instanceof \IPS\Content\Comment ? $object->item()->searchIndexTitle() : $object->searchIndexTitle(), $object->url(), $this->indexData['index_content'], \IPS\DateTime::ts( $this->indexData['index_date_created'] ), NULL, $enclosure );
$document->addItem( $object instanceof \IPS\Content\Comment ? $object->item()->searchIndexTitle() : $object->searchIndexTitle(), $object->url(), \IPS\Content\Search\Result::preDisplay( $this->indexData['index_content'] ), \IPS\DateTime::ts( $this->indexData['index_date_created'] ), NULL, $enclosure );
}
/* If the search result was orphaned, let us continue */
catch( \OutOfRangeException $e ){}
+3 -5
View File
@@ -38,12 +38,10 @@ abstract class _Result
if ( \IPS\Settings::i()->emoji_style == 'twemoji' )
{
return preg_replace( '/(?:' . \IPS\Text\Parser::EMOJI_REGEX . '|\x{200d})+/u', '', $content );
}
else
{
return $content;
$content = preg_replace( '/(?:' . \IPS\Text\Parser::EMOJI_REGEX . '|\x{200d})+/u', '', $content );
}
return $content;
}
/**
+56 -3
View File
@@ -56,6 +56,11 @@ class _Results extends \ArrayIterator
* @brief Reaction Data
*/
protected $reactions = array();
/**
* @brief Attached Images
*/
protected $attachedImages = array();
/**
* @brief Review Ratings
@@ -100,7 +105,8 @@ class _Results extends \ArrayIterator
$reviewRatings = array();
$itemIndexIds = array();
$indexIds = array();
$attachedImages = array();
/* Loop */
foreach ( $this->getArrayCopy() as $result )
{
@@ -162,8 +168,10 @@ class _Results extends \ArrayIterator
{
$containersToLoad[ 'IPS\Member\Club' ][ $result['index_club_id'] ] = $result['index_club_id'];
}
$attachedImages[ $result['index_class'] ][ $result['index_item_id'] ][] = $result['index_object_id'];
}
/* Load item data */
foreach ( $itemsToLoad as $itemClass => $itemIds )
{
@@ -245,7 +253,47 @@ class _Results extends \ArrayIterator
$this->tags = iterator_to_array( \IPS\Db::i()->select( 'index_id, GROUP_CONCAT(index_tag) as index_tags', 'core_search_index_tags', array( array( \IPS\Db::i()->in( 'index_id', $itemIndexIds ) ), array( 'index_is_prefix = 0' ) ), NULL, NULL, 'index_id' )->setKeyField('index_id') );
$this->prefixes = iterator_to_array( \IPS\Db::i()->select( 'index_id, index_tag as index_prefix', 'core_search_index_tags', array( array( \IPS\Db::i()->in( 'index_id', $itemIndexIds ) ), array( 'index_is_prefix = 1' ) ) )->setKeyField('index_id') );
}
/* Load attached images */
$clause = [];
$binds = [];
$locationKeyMap = [];
foreach ( $attachedImages as $class => $ids )
{
$itemClass = \in_array( 'IPS\Content\Comment', class_parents( $class ) ) ? $class::$itemClass : $class;
foreach( $ids as $indexId => $commentIds )
{
foreach( $commentIds as $commentId )
{
$locationKey = $itemClass::$application . '_' . mb_ucfirst( $itemClass::$module );
$locationKeyMap[$locationKey] = $class;
$clause[] = "( location_key=? AND id1=? AND id2=?)";
$binds[] = $locationKey;
$binds[] = $indexId;
$binds[] = $commentId;
}
}
}
if ( \count( $attachedImages ) and \count( $clause ) )
{
/* As we need to join back onto the map table, 2 queries are much more efficient */
$ids = iterator_to_array( \IPS\Db::i()->select( 'attachment_id', 'core_attachments_map', array( array_merge( array( implode( ' OR ', $clause ) ), $binds ) ) ) );
foreach ( \IPS\Db::i()->select( '*', 'core_attachments', array( array( \IPS\Db::i()->in( 'attach_id', $ids ) ), array('attach_is_image=1') ), 'attach_id ASC' )
->join( 'core_attachments_map', 'core_attachments_map.attachment_id=core_attachments.attach_id' ) as $row )
{
if ( isset( $locationKeyMap[ $row['location_key'] ] ) and isset( $row['id2'] ) )
{
$this->attachedImages[ $locationKeyMap[ $row['location_key'] ] ][ $row['id2'] ][] = [
'extension' => 'core_Attachment',
'location' => $row['attach_location'],
'thumb_location' => $row['attach_thumb_location'],
'labels' => implode( ', ', \IPS\Text\Parser::getAttachmentLabels( $row ) )
];
}
}
}
/* Set that we're initiated */
$this->initated = TRUE;
}
@@ -303,6 +351,11 @@ class _Results extends \ArrayIterator
}
}
if ( \is_array( $this->attachedImages ) and isset( $this->attachedImages[ $class ][ $data['index_object_id'] ] ) )
{
$itemData['attachedImages'] = $this->attachedImages[ $class ][ $data['index_object_id'] ];
}
if ( isset( $this->tags[ $data['index_item_index_id'] ] ) )
{
$data['index_tags'] = $this->tags[ $data['index_item_index_id'] ]['index_tags'];
+5
View File
@@ -81,6 +81,8 @@ trait Solvable
$oldComment = $commentClass::load( $this->$solvedField );
$oldComment->setSolved( FALSE );
$oldComment->save();
\IPS\Db::i()->delete( 'core_solved_index', array( 'comment_class=? AND comment_id=?', $commentClass, $oldComment->$commentIdField ) );
}
catch( \Exception $e ) { }
}
@@ -134,6 +136,9 @@ trait Solvable
\IPS\Member::load( $memberToRecount )->recountNotifications();
}
}
/* Update search index */
\IPS\Content\Search\Index::i()->index( $comment );
}
/**
+33 -20
View File
@@ -83,6 +83,7 @@ trait Statistics
catch( \OutOfRangeException $e )
{
$where = $this->_getVisibleWhere();
$where[] = [ $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['author'] . '!=?', 0];
$members = iterator_to_array( \IPS\Db::i()->select( "count(*) as sum, {$authorColumn}", $commentClass::$databaseTable, $where, 'sum DESC', array( 0, $count ), array( $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['author'] ) ) );
@@ -108,6 +109,11 @@ trait Statistics
$return[] = array( 'member' => $member, 'count' => $counts[ $member->member_id ] );
}
usort($return, function ( $member, $member2 )
{
return $member2['count'] <=> $member['count'];
});
return $return;
}
@@ -247,7 +253,7 @@ trait Statistics
public function clearCachedStatistics()
{
$idField = static::$databaseColumnId;
\IPS\Db::i()->delete( 'core_item_statistics_cache', array( 'cache_class=? and cache_item_id=?', \get_class( $this ), $this->$idField ) );
\IPS\Db::i()->delete( 'core_item_statistics_cache', array( 'cache_class=? and cache_item_id=?', \get_class( $this ), $this->$idField ) );
}
/**
@@ -378,17 +384,19 @@ trait Statistics
*/
protected function _getCached( $key )
{
if ( static::$cachedActivity === NULL )
$idField = static::$databaseColumnId;
$class = \get_class( $this );
$arrayKey = $class.'.'.$this->$idField;
if ( !isset( static::$cachedActivity[$arrayKey] ) )
{
$idField = static::$databaseColumnId;
try
{
$cache = \IPS\Db::i()->select( '*', 'core_item_statistics_cache', array( 'cache_class=? and cache_item_id=?', \get_class( $this ), $this->$idField ) )->first();
$cache = \IPS\Db::i()->select( '*', 'core_item_statistics_cache', array( 'cache_class=? and cache_item_id=?', $class, $this->$idField ) )->first();
if ( $cache['cache_added'] > time() - 86400 )
{
static::$cachedActivity = json_decode( $cache['cache_contents'], TRUE );
static::$cachedActivity[ $arrayKey ] = json_decode( $cache['cache_contents'], TRUE );
}
else
{
@@ -398,18 +406,18 @@ trait Statistics
catch( \UnderflowException $e ) { }
}
if ( static::$cachedActivity !== NULL and isset( static::$cachedActivity[ $key ] ) )
if ( isset(static::$cachedActivity[ $arrayKey ] ) and isset( static::$cachedActivity[ $arrayKey ][ $key ] ) )
{
return static::$cachedActivity[ $key ];
return static::$cachedActivity[ $arrayKey ][ $key ];
}
else
{
if( static::$cachedActivity === NULL )
if( !isset( static::$cachedActivity[ $arrayKey ] ) )
{
static::$cachedActivity = array();
static::$cachedActivity[ $arrayKey ] = array();
}
static::$cachedActivity[ $key ] = array();
static::$cachedActivity[ $arrayKey ][ $key ] = array();
throw new \OutOfRangeException;
}
}
@@ -435,10 +443,12 @@ trait Statistics
catch( \Exception $e ) { }
$idField = static::$databaseColumnId;
static::$cachedActivity[ $key ] = $value;
$class = \get_class( $this );
$arrayKey = $class.'.'.$this->$idField;
$this->storeCache = $this->$idField;
static::$cachedActivity[ $arrayKey ][ $key ] = $value;
$this->storeCache[ $arrayKey ][ $key ] = $this->$idField;
}
/**
@@ -450,12 +460,15 @@ trait Statistics
{
if( $this->storeCache )
{
\IPS\Db::i()->insert( 'core_item_statistics_cache', array(
'cache_class' => \get_class( $this ),
'cache_item_id' => $this->storeCache,
'cache_contents' => json_encode( static::$cachedActivity ),
'cache_added' => time()
), TRUE );
foreach( $this->storeCache as $key => $data )
{
\IPS\Db::i()->insert( 'core_item_statistics_cache', array(
'cache_class' => \get_class( $this ),
'cache_item_id' => str_replace( \get_class($this) . '.', '', $key),
'cache_contents' => json_encode( static::$cachedActivity[$key] ),
'cache_added' => time()
), TRUE );
}
}
if( \is_callable( 'parent::__destruct' ) )
+3 -2
View File
@@ -201,7 +201,8 @@ abstract class _Widget extends \IPS\Widget\PermissionCache
/* Sort */
$sortOptions = $class::getWidgetSortOptions();
$return['sort'] = new \IPS\Helpers\Form\Select( 'widget_feed_sort_on', isset( $this->configuration['widget_feed_sort_on'] ) ? $this->configuration['widget_feed_sort_on'] : $class::$databaseColumnMap['updated'], FALSE, array( 'options' => $sortOptions ), NULL, NULL, NULL, 'widget_feed_sort_on' );
$dateColumn = $class::$databasePrefix . ( isset( $class::$databaseColumnMap['updated'] ) ? $class::$databaseColumnMap['updated'] : $class::$databaseColumnMap['date'] );
$return['sort'] = new \IPS\Helpers\Form\Select( 'widget_feed_sort_on', $this->configuration['widget_feed_sort_on'] ?? $dateColumn, FALSE, array( 'options' => $sortOptions ), NULL, NULL, NULL, 'widget_feed_sort_on' );
$return['direction'] = new \IPS\Helpers\Form\Select( 'widget_feed_sort_dir', isset( $this->configuration['widget_feed_sort_dir'] ) ? $this->configuration['widget_feed_sort_dir'] : 'desc', FALSE, array(
'options' => array(
@@ -439,7 +440,7 @@ abstract class _Widget extends \IPS\Widget\PermissionCache
$binds = array( $class::$application, $class::$module );
foreach( $this->configuration['widget_feed_tags'] as $tag )
{
$tagWhere[] = "( tag_text LIKE CONCAT( ?, '%') )";
$tagWhere[] = "( tag_text=? )";
$binds[] = $tag;
}
+56 -21
View File
@@ -43,6 +43,16 @@ abstract class _CustomField extends \IPS\Node\Model
* @brief [CustomField] Additional Field Toggles
*/
public static $additionalFieldToggles = array();
/**
* @brief [CustomField] An array of the 'keys' of the Field types toggles that shouldn't be an option. The main use is for excluding polls
*/
public static $disabledFieldTypes = array();
/**
* @brief [CustomField] Set to TRUE if uploads fields are capable of holding the submitted content for moderation
*/
public static $uploadsCanBeModerated = FALSE;
/**
* Get
@@ -96,6 +106,14 @@ abstract class _CustomField extends \IPS\Node\Model
public function form( &$form )
{
$form->addHeader( 'pfield_settings' );
/* Are we being forced to change the field type? */
if ( !$this->_new AND isset( $this->type ) AND \in_array( $this->type, static::$disabledFieldTypes ) )
{
//\IPS\Member::loggedIn()->language()->words['pf_type_deprecated'] = \IPS\Member::loggedIn()->language()->addToStack('custom_field_type_deprecated');
$form->addMessage( 'custom_field_type_deprecated', 'ipsMessage ipsMessage_info ipsMargin' );
}
$form->add( new \IPS\Helpers\Form\Translatable( 'pf_title', NULL, TRUE, array( 'app' => 'core', 'key' => ( $this->id ? static::$langKey . '_' . $this->id : NULL ) ) ) );
$form->add( new \IPS\Helpers\Form\Translatable( 'pf_desc', NULL, FALSE, array( 'app' => 'core', 'key' => ( $this->id ? static::$langKey . '_' . $this->id . '_desc' : NULL ) ) ) );
@@ -127,28 +145,36 @@ abstract class _CustomField extends \IPS\Node\Model
'Url' => 'pf_type_Url',
'YesNo' => 'pf_type_YesNo',
), static::$additionalFieldTypes );
$toggles = array(
'CheckboxSet' => array( 'pf_content', 'pf_not_null', 'pf_search_type_on_off', "{$form->id}_header_pfield_displayoptions", $this->fieldFormattingId ),
'Codemirror' => array( 'pf_not_null', 'pf_max_input', "{$form->id}_header_pfield_displayoptions", $this->fieldFormattingId ),
'Email' => array( 'pf_not_null', 'pf_max_input', 'pf_input_format', 'pf_search_type', "{$form->id}_header_pfield_displayoptions", $this->fieldFormattingId ),
'Member' => array( 'pf_not_null', 'pf_multiple', "{$form->id}_header_pfield_displayoptions", $this->fieldFormattingId ),
'Password' => array( 'pf_not_null', 'pf_max_input', 'pf_input_format', "{$form->id}_header_pfield_displayoptions", $this->fieldFormattingId ),
'Select' => array( 'pf_not_null', 'pf_content', 'pf_multiple', 'pf_search_type_on_off', "{$form->id}_header_pfield_displayoptions", $this->fieldFormattingId ),
'Tel' => array( 'pf_not_null', 'pf_max_input', 'pf_input_format', 'pf_search_type', "{$form->id}_header_pfield_displayoptions", $this->fieldFormattingId ),
'Text' => array( 'pf_not_null', 'pf_max_input', 'pf_input_format', 'pf_search_type', "{$form->id}_header_pfield_displayoptions", $this->fieldFormattingId ),
'TextArea' => array( 'pf_not_null', 'pf_max_input', 'pf_input_format', 'pf_search_type', "{$form->id}_header_pfield_displayoptions", $this->fieldFormattingId ),
'Url' => array( 'pf_not_null', 'pf_max_input', 'pf_input_format', 'pf_search_type', "{$form->id}_header_pfield_displayoptions", $this->fieldFormattingId ),
'Radio' => array( 'pf_content', "{$form->id}_header_pfield_displayoptions", 'pf_search_type_on_off', $this->fieldFormattingId ),
'Address' => array( 'pf_not_null', "{$form->id}_header_pfield_displayoptions", $this->fieldFormattingId ),
'Color' => array( 'pf_not_null', "{$form->id}_header_pfield_displayoptions", $this->fieldFormattingId ),
'Date' => array( 'pf_not_null', "{$form->id}_header_pfield_displayoptions", $this->fieldFormattingId ),
'Editor' => array( 'pf_not_null', "{$form->id}_header_pfield_displayoptions", $this->fieldFormattingId, 'pf_search_type', 'pf_allow_attachments' ),
'Number' => array( 'pf_not_null', "{$form->id}_header_pfield_displayoptions", $this->fieldFormattingId ),
'CheckboxSet' => array( 'pf_content', 'pf_not_null', 'pf_search_type_on_off', "{$form->id}_header_pfield_displayoptions" ),
'Codemirror' => array( 'pf_not_null', 'pf_max_input', "{$form->id}_header_pfield_displayoptions" ),
'Email' => array( 'pf_not_null', 'pf_max_input', 'pf_input_format', 'pf_search_type', "{$form->id}_header_pfield_displayoptions" ),
'Member' => array( 'pf_not_null', 'pf_multiple', "{$form->id}_header_pfield_displayoptions" ),
'Password' => array( 'pf_not_null', 'pf_max_input', 'pf_input_format', "{$form->id}_header_pfield_displayoptions" ),
'Select' => array( 'pf_not_null', 'pf_content', 'pf_multiple', 'pf_search_type_on_off', "{$form->id}_header_pfield_displayoptions" ),
'Tel' => array( 'pf_not_null', 'pf_max_input', 'pf_input_format', 'pf_search_type', "{$form->id}_header_pfield_displayoptions" ),
'Text' => array( 'pf_not_null', 'pf_max_input', 'pf_input_format', 'pf_search_type', "{$form->id}_header_pfield_displayoptions" ),
'TextArea' => array( 'pf_not_null', 'pf_max_input', 'pf_input_format', 'pf_search_type', "{$form->id}_header_pfield_displayoptions" ),
'Url' => array( 'pf_not_null', 'pf_max_input', 'pf_input_format', 'pf_search_type', "{$form->id}_header_pfield_displayoptions" ),
'Radio' => array( 'pf_content', "{$form->id}_header_pfield_displayoptions", 'pf_search_type_on_off' ),
'Address' => array( 'pf_not_null', "{$form->id}_header_pfield_displayoptions" ),
'Color' => array( 'pf_not_null', "{$form->id}_header_pfield_displayoptions" ),
'Date' => array( 'pf_not_null', "{$form->id}_header_pfield_displayoptions" ),
'Editor' => array( 'pf_not_null', "{$form->id}_header_pfield_displayoptions", 'pf_search_type', 'pf_allow_attachments' ),
'Number' => array( 'pf_not_null', "{$form->id}_header_pfield_displayoptions"),
'Poll' => array( 'pf_not_null' ),
'Rating' => array( 'pf_not_null', "{$form->id}_header_pfield_displayoptions", $this->fieldFormattingId ),
'Upload' => array( 'pf_not_null', "{$form->id}_header_pfield_displayoptions", $this->fieldFormattingId ),
'Rating' => array( 'pf_not_null', "{$form->id}_header_pfield_displayoptions" ),
'Upload' => array( 'pf_not_null', "{$form->id}_header_pfield_displayoptions" ),
);
foreach( $options as $id => $toggle )
{
$toggles[$id][] = $this->fieldFormattingId;
}
foreach ( static::$additionalFieldTypes as $k => $v )
{
$toggles[ $k ] = isset( static::$additionalFieldToggles[ $k ] ) ? static::$additionalFieldToggles[ $k ] : array( 'pf_not_null' );
@@ -164,13 +190,20 @@ abstract class _CustomField extends \IPS\Node\Model
$toggles[ $k ] = $v;
}
}
/* Remove the disabled fields */
foreach( static::$disabledFieldTypes as $type )
{
unset( $options[ $type ] );
unset( $toggles[ $type ] );
}
ksort( $options );
if ( !$this->_new )
{
\IPS\Member::loggedIn()->language()->words['pf_type_warning'] = \IPS\Member::loggedIn()->language()->addToStack('custom_field_change');
foreach ( $toggles as $k => $_toggles )
{
if ( !$this->canKeepValueOnChange( $k ) )
@@ -746,7 +779,8 @@ abstract class _CustomField extends \IPS\Node\Model
case 'Upload':
$options['storageExtension'] = static::$uploadStorageExtension;
if( $value )
$options['canBeModerated'] = static::$uploadsCanBeModerated;
if( $value )
{
try
{
@@ -775,6 +809,7 @@ abstract class _CustomField extends \IPS\Node\Model
break;
case 'Member':
$options['autocomplete']['minimized'] = false;
$options['multiple'] = $this->multiple ? NULL : 1;
if ( $value )
{
@@ -1020,7 +1055,7 @@ abstract class _CustomField extends \IPS\Node\Model
return ( $value AND $value !== '#' ) ? \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->colorDisplay( $value ) : '';
case 'Date':
return \IPS\DateTime::ts( $value, TRUE )->localeDate(); // See buildHelper for why we ignore the timezone
return $value ? \IPS\DateTime::ts( $value, TRUE )->localeDate() : ''; // See buildHelper for why we ignore the timezone
case 'Editor':
return $value;
+41 -1
View File
@@ -32,7 +32,7 @@ class _DateTime extends \DateTime
public static function ts( $timestamp, $bypassTimezone=FALSE )
{
$obj = new static;
$obj->setTimestamp( $timestamp );
$obj->setTimestamp( (int) $timestamp );
if ( !$bypassTimezone AND \IPS\Dispatcher::hasInstance() and \IPS\Member::loggedIn()->timezone )
{
$validTimezone = TRUE;
@@ -86,6 +86,21 @@ class _DateTime extends \DateTime
return static::$timeZoneIdentifiers;
}
/**
* Helper method to fix Argentinian Timezones https://bugs.webkit.org/show_bug.cgi?id=218542
*
* @param string $timezone
* @return string|null
*/
public static function getFixedTimezone( ?string $timezone=NULL ) :? string
{
if ( \in_array( $timezone, [ 'America/Buenos_Aires', 'America/Catamarca', 'America/Cordoba', 'America/Jujuy', 'America/La_Rioja', 'America/Mendoza', 'America/Rio_Gallegos', 'America/Salta', 'America/San_Juan', 'America/San_Luis', 'America/Tucuman', 'America/Ushuaia' ] ) )
{
return 'America/Argentina/' . \substr( $timezone, 8 );
}
return $timezone;
}
/**
* Create New
@@ -199,6 +214,31 @@ class _DateTime extends \DateTime
return "<time datetime='{$this->rfc3339()}' title='{$this}' data-short='" . trim( $this->relative( 1, $memberOrLanguage ) ) . "'>" . trim( $this->relative( $format, $memberOrLanguage ) ) . "</time>";
}
/**
* Get total number of days from DateInterval
*
* @param \DateInterval $interval DateInterval
* @return int
*/
static public function intervalToDays( \DateInterval $interval ): int
{
$days = 0;
if ( $interval->y )
{
$days += ( 365 * $interval->y );
}
if ( $interval->m )
{
$days += ( ( 365 / 12 ) * $interval->m );
}
if ( $interval->d )
{
$days += $interval->d;
}
return $days;
}
/**
* Format the date according to the user's locale (without the time)
*
+13 -3
View File
@@ -17,6 +17,9 @@ if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
exit;
}
// Make sure PHP 8.1 uses MySQL errors as we expect
\mysqli_report( MYSQLI_REPORT_OFF );
/**
* @brief Database Class
* @note All functionality MUST be supported by MySQL 5.1.3 and higher. All references to the MySQL manual are therefore the 5.1 version.
@@ -904,7 +907,9 @@ class _Db extends \mysqli
}
/* Return */
return new \IPS\Db\Select( $query, $binds, $this );
$return = new \IPS\Db\Select( $query, $binds, $this );
$return->isUnion = TRUE;
return $return;
}
/**
@@ -1026,6 +1031,11 @@ class _Db extends \mysqli
$query = "{$type} INTO `{$this->prefix}{$table}` " . $set->query;
$binds = $set->binds;
}
elseif ( \is_array( $set ) and \count( $set ) == 2 and isset( $set[1] ) and $set[1] instanceof \IPS\Db\Select )
{
$query = "{$type} INTO `{$this->prefix}{$table}` (" . $set[0] . ") " . $set[1]->query;
$binds = $set[1]->binds;
}
else
{
/* Is this just one row? */
@@ -1088,7 +1098,7 @@ class _Db extends \mysqli
$binds = array();
/* Work out table */
$table = \is_array( $table ) ? "`{$this->prefix}{$table[0]}` {$this->prefix}{$table[1]}" : "`{$this->prefix}{$table}` {$table}";
$table = \is_array( $table ) ? "`{$this->prefix}{$table[0]}` `{$this->prefix}{$table[1]}`" : "`{$this->prefix}{$table}` `{$table}`";
/* Work out joins */
$_joins = array();
@@ -1236,7 +1246,7 @@ class _Db extends \mysqli
{
$alias = \is_string( $alias ) ? $alias : $_table;
$tables[] = "`{$this->prefix}{$_table}` AS {$alias}";
$tables[] = "`{$this->prefix}{$_table}` AS `{$alias}`";
}
$query .= implode( ', ', $tables );
+7 -2
View File
@@ -53,9 +53,14 @@ class _Select implements \Iterator, \Countable
protected $multiDimensional = FALSE;
/**
* @brief If read/write sepration is enabled, SELECTs normally go to the read server - this flag will set thia query to use the write server
* @brief If read/write separation is enabled, SELECTs normally go to the read server - this flag will set this query to use the write server
*/
protected $useWriteServer = FALSE;
/**
* @brief Is this a union query? - this should only be set by the UNION method in \IPS\Db
*/
public $isUnion = FALSE;
/**
* Constuctor
@@ -224,7 +229,7 @@ class _Select implements \Iterator, \Countable
$this->query = str_replace( $matches[0], $key, $this->query );
}
if ( mb_strstr( $this->query, 'UNION' ) )
if ( $this->isUnion )
{
$this->query = str_replace( 'derivedTable', "derivedTable {$query}", $this->query );
}
+72 -60
View File
@@ -143,93 +143,105 @@ class _Developer
/* Run queries for previous versions */
if ( static::$lastUpdates[ $this->app->directory ]['working_version'] != $latestVersion )
{
/* Get all versions past the working version */
$dirMatches = [];
foreach ( new \DirectoryIterator( \IPS\ROOT_PATH . "/applications/{$this->app->directory}/setup/" ) as $dir )
{
if ( $dir->isDir() and !$dir->isDot() and preg_match( '/^upg_(\d+)$/', $dir, $matches ) )
{
if ( (int) $matches[1] == static::$lastUpdates[ $this->app->directory ]['working_version'] )
if ( (int) $matches[1] >= static::$lastUpdates[ $this->app->directory ]['working_version'] )
{
if( file_exists( \IPS\ROOT_PATH . "/applications/{$this->app->directory}/setup/upg_{$matches[1]}/queries.json" ) )
$dirMatches[] = (int) $matches[1];
}
}
}
/* Run through the *sorted* versions. Note DirectoryIterator sorts by last modified, but you want this to run in order of versions */
asort( $dirMatches );
foreach ( $dirMatches as $match )
{
if ( $match == static::$lastUpdates[ $this->app->directory ]['working_version'] )
{
if( file_exists( \IPS\ROOT_PATH . "/applications/{$this->app->directory}/setup/upg_{$match}/queries.json" ) )
{
$queries = json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->app->directory}/setup/upg_{$match}/queries.json" ), TRUE );
$localQueries = json_decode( static::$lastUpdates[ $this->app->directory ]['ran'], TRUE );
foreach ( $queries as $q )
{
$queries = json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->app->directory}/setup/upg_{$matches[1]}/queries.json" ), TRUE );
$localQueries = json_decode( static::$lastUpdates[ $this->app->directory ]['ran'], TRUE );
foreach ( $queries as $q )
if ( \is_array( $localQueries ) AND !\in_array( $q, $localQueries ) )
{
if ( \is_array( $localQueries ) AND !\in_array( $q, $localQueries ) )
{
$method = $q['method'];
$params = $q['params'];
\IPS\Db::i()->$method( ...$params );
}
$method = $q['method'];
$params = $q['params'];
\IPS\Db::i()->$method( ...$params );
}
}
}
elseif ( (int) $matches[1] > static::$lastUpdates[ $this->app->directory ]['working_version'] )
}
else
{
if( file_exists( \IPS\ROOT_PATH . "/applications/{$this->app->directory}/setup/upg_{$match}/queries.json" ) )
{
if( file_exists( \IPS\ROOT_PATH . "/applications/{$this->app->directory}/setup/upg_{$matches[1]}/queries.json" ) )
$queries = json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->app->directory}/setup/upg_{$match}/queries.json" ), TRUE );
foreach ( $queries as $q )
{
$queries = json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->app->directory}/setup/upg_{$matches[1]}/queries.json" ), TRUE );
foreach ( $queries as $q )
try
{
try
$method = $q['method'];
$params = $q['params'];
\IPS\Db::i()->$method( ...$params );
}
catch( \IPS\Db\Exception $e )
{
/* If the issue is with a create table other than exists, we should just throw it */
if ( $q['method'] == 'createTable' and ! \in_array( $e->getCode(), array( 1007, 1050 ) ) )
{
$method = $q['method'];
$params = $q['params'];
\IPS\Db::i()->$method( ...$params );
throw $e;
}
catch( \IPS\Db\Exception $e )
/* Can't change a column as it doesn't exist */
if ( $e->getCode() == 1054 )
{
/* If the issue is with a create table other than exists, we should just throw it */
if ( $q['method'] == 'createTable' and ! \in_array( $e->getCode(), array( 1007, 1050 ) ) )
if ( $q['method'] == 'changeColumn' )
{
throw $e;
}
/* Can't change a column as it doesn't exist */
if ( $e->getCode() == 1054 )
{
if ( $q['method'] == 'changeColumn' )
if ( \IPS\Db::i()->checkForTable( $q['params'][0] ) )
{
if ( \IPS\Db::i()->checkForTable( $q['params'][0] ) )
/* Does the column exist already? */
if ( \IPS\Db::i()->checkForColumn( $q['params'][0], $q['params'][2]['name'] ) )
{
/* Does the column exist already? */
if ( \IPS\Db::i()->checkForColumn( $q['params'][0], $q['params'][2]['name'] ) )
{
/* Just make sure it's up to date */
\IPS\Db::i()->changeColumn( $q['params'][0], $q['params'][2]['name'], $q['params'][2] );
continue;
}
else
{
/* The table exists, so lets just add the column */
\IPS\Db::i()->addColumn( $q['params'][0], $q['params'][2] );
continue;
}
/* Just make sure it's up to date */
\IPS\Db::i()->changeColumn( $q['params'][0], $q['params'][2]['name'], $q['params'][2] );
continue;
}
}
throw $e;
}
/* Can't rename a table as it doesn't exist */
else if ( $e->getCode() == 1017 )
{
if ( $q['method'] == 'renameTable' )
{
if ( \IPS\Db::i()->checkForTable( $q['params'][1] ) )
else
{
/* The table we are renaming to *does* exist */
/* The table exists, so lets just add the column */
\IPS\Db::i()->addColumn( $q['params'][0], $q['params'][2] );
continue;
}
}
throw $e;
}
/* If the error isn't important we should ignore it */
else if( !\in_array( $e->getCode(), array( 1007, 1008, 1050, 1060, 1061, 1062, 1091 ) ) )
throw $e;
}
/* Can't rename a table as it doesn't exist */
else if ( $e->getCode() == 1017 )
{
if ( $q['method'] == 'renameTable' )
{
throw $e;
if ( \IPS\Db::i()->checkForTable( $q['params'][1] ) )
{
/* The table we are renaming to *does* exist */
continue;
}
}
throw $e;
}
/* If the error isn't important we should ignore it */
else if( !\in_array( $e->getCode(), array( 1007, 1008, 1050, 1060, 1061, 1062, 1091 ) ) )
{
throw $e;
}
}
}
+83 -3
View File
@@ -206,7 +206,12 @@ class _Admin extends \IPS\Dispatcher\Standard
{
\IPS\Output::i()->error( 'no_module_permission', '2S107/1', 403, '' );
}
if( ! \IPS\Application::appIsEnabled( $this->application->directory ) )
{
\IPS\Output::i()->error( 'requested_route_404', '2S107/5', 404, '' );
}
/* Support is not available for demos */
if ( \IPS\DEMO_MODE AND $this->module->application === 'core' AND $this->module->key === 'support' )
{
@@ -515,7 +520,18 @@ class _Admin extends \IPS\Dispatcher\Standard
return $this->acpTabOrder;
}
/**
* Display a link in a custom format
*
* @param string $url The URL from the menu system
* @return string|null
*/
public function acpMenuCustom( $url ): ?string
{
return NULL;
}
/**
* Do we have permission to use this module?
*
@@ -534,13 +550,77 @@ class _Admin extends \IPS\Dispatcher\Standard
* @param string $key Permission Key
* @param \IPS\Application|null $app Application (NULL will default to current)
* @param \IPS\Module|string|null $module Module (NULL will default to current)
* @param boolean $return Return boolean (true/false) instead of throwing an error
* @return void
*/
public function checkAcpPermission( $key, $app=NULL, $module=NULL )
public function checkAcpPermission( $key, $app=NULL, $module=NULL, $return=FALSE )
{
if ( !\IPS\Member::loggedIn()->hasAcpRestriction( ( $app ?: $this->application ), ( $module ?: $this->module ), $key ) )
{
if ( $return )
{
return FALSE;
}
\IPS\Output::i()->error( 'no_module_permission', '2S107/2', 403, '' );
}
if ( $return )
{
return TRUE;
}
}
/**
* Show switch link
*
* @return boolean
*/
final public static function showSwitchLink(): bool
{
if ( \IPS\CIC )
{
return false;
}
/* Don't show if installation was less than a month ago */
if ( time() < \IPS\Settings::i()->board_start + ( 86400 * 30 ) )
{
return false;
}
if ( ! isset( \IPS\Request::i()->cookie['acpLinkSnooze'] ) )
{
return true;
}
$value = json_decode( \IPS\Request::i()->cookie['acpLinkSnooze'], true );
if ( $value['hits'] < 3 )
{
/* Show every 30 days */
if ( time() < ( $value['lastClick'] + ( 86400 * 30 ) ) )
{
return false;
}
}
else if ( $value['hits'] < 9 )
{
/* Show every 60 days */
if ( time() < ( $value['lastClick'] + ( 86400 * 60 ) ) )
{
return false;
}
}
else
{
/* Show every 90 days */
if ( time() < ( $value['lastClick'] + ( 86400 * 90 ) ) )
{
return false;
}
}
return true;
}
}
+1 -1
View File
@@ -408,7 +408,7 @@ class _Api extends \IPS\Dispatcher
*/
protected function _respond( $response, $httpResponseCode, $oauthError=NULL, $log=FALSE )
{
$headers = ( mb_strtolower( $_SERVER['REQUEST_METHOD'] ) == 'get' ) ? \IPS\Output::getCacheHeaders( time(), 60 ) : \IPS\Output::getNoCacheHeaders();
$headers = ( mb_strtolower( $_SERVER['REQUEST_METHOD'] ) == 'get' AND !$this->rawAccessToken ) ? \IPS\Output::getCacheHeaders( time(), 60 ) : \IPS\Output::getNoCacheHeaders();
if ( $this->rawAccessToken and $oauthError )
{
+17
View File
@@ -31,6 +31,15 @@ abstract class _Controller
* @brief Is this for displaying "content"? Affects if advertisements may be shown
*/
public $isContentPage = TRUE;
/**
* @brief These properties are used to specify datalayer context properties.
*
* @example array(
'community_area' => array( 'value' => 'search', 'odkUpdate' => 'true' )
* )
*/
public static $dataLayerContext = array();
/**
* Constructor
@@ -82,6 +91,14 @@ abstract class _Controller
*/
public function execute()
{
if ( !empty( static::$dataLayerContext ) AND \IPS\Settings::i()->core_datalayer_enabled AND !\IPS\Request::i()->isAjax() )
{
foreach ( static::$dataLayerContext as $property => $data )
{
\IPS\core\DataLayer::i()->addContextProperty( $property, $data['value'], $data['odkUpdate'] ?? false );
}
}
if( \IPS\Request::i()->do and preg_match( '/^[a-zA-Z\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', \IPS\Request::i()->do ) )
{
if ( method_exists( $this, \IPS\Request::i()->do ) or method_exists( $this, '__call' ) )
+18 -2
View File
@@ -26,7 +26,7 @@ abstract class _Dispatcher
* @brief Singleton Instance
*/
protected static $instance = NULL;
/**
* Check if a dispatcher instance is available
*
@@ -170,7 +170,23 @@ abstract class _Dispatcher
else
{
/* Just prefetch this to save a query later */
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( \IPS\Output::i()->title, \IPS\Output::i()->output, array( 'app' => \IPS\Dispatcher::i()->application->directory, 'module' => \IPS\Dispatcher::i()->module->key, 'controller' => \IPS\Dispatcher::i()->controller ) ), 200, 'text/html' );
\IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( \IPS\Output::i()->title, \IPS\Output::i()->output, $this->getLocationData() ), 200, 'text/html' );
}
}
/**
* Get an array of data representing the user's current location
* This gets passed to the templates in order to apply some attributes to the body tag
*
* @return array
*/
public function getLocationData()
{
return array(
'app' => \IPS\Dispatcher::i()->application->directory,
'module' => \IPS\Dispatcher::i()->module->key,
'controller' => \IPS\Dispatcher::i()->controller,
'id' => \IPS\Request::i()->id ? (int) \IPS\Request::i()->id : NULL
);
}
}
+216 -85
View File
@@ -191,83 +191,15 @@ class _Front extends \IPS\Dispatcher\Standard
/* Do we need more info from the member or do they need to validate? */
/* This controllers should always be accessible, no matter if the member is awaiting validation or needs to set up the email or name */
$legalControllers = array( 'privacy', 'contact', 'terms', 'embed', 'metatags', 'store', 'checkout', 'subscriptions' );
/* These controllers should always be accessible, no matter if the member is awaiting validation or needs to set up the email or name */
$legalControllers = array( 'privacy', 'contact', 'terms', 'embed', 'metatags', 'subscriptions', 'serviceworker', 'settings' );
/* Do we need more info from the member or do they need to validate? */
if( \IPS\Member::loggedIn()->member_id and $this->controller !== 'language' and $this->controller !== 'theme' and $this->controller !== 'ajax' and !\in_array( $this->controller, $legalControllers ) )
{
/* Need their name or email... */
if( ( \IPS\Member::loggedIn()->real_name === '' or !\IPS\Member::loggedIn()->email ) and $this->controller !== 'register' and $this->controller !== 'login' )
if ( $url = static::doMemberCheck() )
{
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=system&controller=register&do=complete' )->setQueryString( 'ref', base64_encode( \IPS\Request::i()->url() ) ) );
}
/* Need them to validate... */
elseif( \IPS\Member::loggedIn()->members_bitoptions['validating'] and $this->controller !== 'register' and $this->controller !== 'login' and $this->controller != 'redirect' )
{
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=system&controller=register&do=validating', 'front', 'register' ) );
}
/* Need them to reconfirm terms/privacy policy... */
elseif ( ( \IPS\Member::loggedIn()->members_bitoptions['must_reaccept_privacy'] or \IPS\Member::loggedIn()->members_bitoptions['must_reaccept_terms'] ) and $this->controller !== 'register' and $this->controller !== 'ajax' )
{
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=system&controller=register&do=reconfirm', 'front', 'register' )->setQueryString( 'ref', base64_encode( \IPS\Request::i()->url() ) ) );
}
/* Have required profile actions that need completing */
else if ( \IPS\Settings::i()->quick_register AND !\IPS\Member::loggedIn()->members_bitoptions['profile_completed'] AND !\in_array( $this->controller, array( 'register', 'login', 'redirect', 'ajax', 'settings', 'checkout', 'pixabay', 'editor' ) ) AND $completion = \IPS\Member::loggedIn()->profileCompletion() AND \count( $completion['required'] ) )
{
foreach( $completion['required'] AS $id => $completed )
{
if ( $completed === FALSE )
{
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( "app=core&module=system&controller=register&do=finish&_new=1", 'front', 'register' )->setQueryString( 'ref', base64_encode( \IPS\Request::i()->url() ) ) );
}
}
}
/* Need to set up MFA... */
if ( !\in_array( $this->controller, array( 'register', 'login', 'redirect', 'ajax', 'settings', 'embed' ) ) )
{
$haveAcceptableHandlers = FALSE;
$haveConfiguredHandler = FALSE;
foreach ( \IPS\MFA\MFAHandler::handlers() as $key => $handler )
{
if ( $handler->isEnabled() and $handler->memberCanUseHandler( \IPS\Member::loggedIn() ) )
{
$haveAcceptableHandlers = TRUE;
if ( $handler->memberHasConfiguredHandler( \IPS\Member::loggedIn() ) )
{
$haveConfiguredHandler = TRUE;
break;
}
}
}
if ( !$haveConfiguredHandler and $haveAcceptableHandlers )
{
if ( \IPS\Settings::i()->mfa_required_groups == '*' or \IPS\Member::loggedIn()->inGroup( explode( ',', \IPS\Settings::i()->mfa_required_groups ) ) )
{
if ( \IPS\Settings::i()->mfa_required_prompt === 'immediate' )
{
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=system&controller=settings&do=initialMfa', 'front', 'settings' )->setQueryString( 'ref', base64_encode( \IPS\Request::i()->url() ) ) );
}
}
elseif ( \IPS\Settings::i()->mfa_optional_prompt === 'immediate' and !\IPS\Member::loggedIn()->members_bitoptions['security_questions_opt_out'] )
{
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=system&controller=settings&do=initialMfa', 'front', 'settings' )->setQueryString( 'ref', base64_encode( \IPS\Request::i()->url() ) ) );
}
}
}
/* Need to reset password */
if ( $this->controller !== 'settings' AND ( !isset( \IPS\Request::i()->area ) OR \IPS\Request::i()->area !== 'password' ) )
{
foreach( \IPS\Login::methods() AS $method )
{
if ( $url = $method->forcePasswordResetUrl( \IPS\Member::loggedIn(), \IPS\Request::i()->url() ) )
{
\IPS\Output::i()->redirect( $url );
}
}
\IPS\Output::i()->redirect( $url );
}
}
@@ -287,7 +219,10 @@ class _Front extends \IPS\Dispatcher\Standard
{
\IPS\Output::i()->error( 'requested_route_404', '2S160/5', 404, '' );
}
/* Set up isAnonymous variable for realtime */
\IPS\Output::i()->jsVars['isAnonymous'] = (bool) \IPS\Member::loggedIn()->isOnlineAnonymously();
/* Stuff for output */
if ( !\IPS\Request::i()->isAjax() )
{
@@ -356,7 +291,7 @@ class _Front extends \IPS\Dispatcher\Standard
if ( \IPS\Settings::i()->use_friendly_urls )
{
$url = \IPS\Request::i()->url();
/* Redirect to the "correct" friendly URL if there is one */
if ( !\IPS\Request::i()->isAjax() and mb_strtolower( $_SERVER['REQUEST_METHOD'] ) == 'get' and !\IPS\ENFORCE_ACCESS )
{
@@ -368,13 +303,23 @@ class _Front extends \IPS\Dispatcher\Standard
$correctUrl = $url->correctFriendlyUrl();
}
/* If they are accessing "index.php/whatever", we want "index.php?/whatever */
if ( !( $correctUrl instanceof \IPS\Http\Url ) and $url instanceof \IPS\Http\Url\Internal and mb_strpos( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], '/index.php/' ) !== FALSE )
if ( !( $correctUrl instanceof \IPS\Http\Url ) and $url instanceof \IPS\Http\Url\Internal )
{
$pathFromBaseUrl = ltrim( mb_substr( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], mb_strlen( \IPS\Http\Url::internal('')->data[ \IPS\Http\Url::COMPONENT_PATH ] ) ), '/' );
if ( mb_substr( $pathFromBaseUrl, 0, 10 ) === 'index.php/' )
/* If they are accessing "index.php/whatever", we want "index.php?/whatever */
if ( mb_strpos( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], '/index.php/' ) !== FALSE )
{
$correctUrl = \IPS\Http\Url\Friendly::friendlyUrlFromComponent( 0, trim( mb_substr( $pathFromBaseUrl, 10 ), '/' ), $url->queryString );
if ( mb_substr( $pathFromBaseUrl, 0, 10 ) === 'index.php/' )
{
$correctUrl = \IPS\Http\Url\Friendly::friendlyUrlFromComponent( 0, trim( mb_substr( $pathFromBaseUrl, 10 ), '/' ), $url->queryString );
}
}
else
{
/* If necessary, return any special cases like the robots.txt file */
$this->customResponse( $pathFromBaseUrl );
}
}
@@ -451,16 +396,20 @@ class _Front extends \IPS\Dispatcher\Standard
protected function checkCached()
{
/* If this is a guest and there's a full cached page, we can serve that */
if( mb_strtolower( $_SERVER['REQUEST_METHOD'] ) == 'get' and !isset( \IPS\Request::i()->csrfKey ) and !isset( \IPS\Request::i()->_mfaLogin ) and !\IPS\Session\Front::loggedIn() and !isset( \IPS\Request::i()->cookie['noCache'] ) and \IPS\CACHE_PAGE_TIMEOUT )
if( mb_strtolower( $_SERVER['REQUEST_METHOD'] ) == 'get' AND !isset( \IPS\Request::i()->csrfKey ) AND !isset( \IPS\Request::i()->_mfaLogin ) AND !\IPS\Session\Front::loggedIn() AND !isset( \IPS\Request::i()->cookie['noCache'] ) AND \IPS\CACHE_PAGE_TIMEOUT )
{
/* Check whether a 304 Not modified response is appropriate */
if( !empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) AND ( new \IPS\DateTime( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) )->getTimestamp() > ( time() - \IPS\CACHE_PAGE_TIMEOUT ) )
try
{
/* This is intentionally using PHP methods to avoid database connections */
$this->destruct = false;
header('HTTP/1.1 304 Not Modified' );
exit;
if( !empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) AND ( new \IPS\DateTime( \rtrim( $_SERVER['HTTP_IF_MODIFIED_SINCE'], ';' ) ) )->getTimestamp() > ( time() - \IPS\CACHE_PAGE_TIMEOUT ) )
{
/* This is intentionally using PHP methods to avoid database connections */
$this->destruct = false;
header('HTTP/1.1 304 Not Modified' );
exit;
}
}
catch ( \Exception $e ) { }
/* Only check the output cache if it is enabled */
if( empty( \IPS\OUTPUT_CACHE_METHOD ) OR ( !empty( \IPS\OUTPUT_CACHE_METHOD ) AND \IPS\OUTPUT_CACHE_METHOD == 'None' ) )
@@ -660,18 +609,24 @@ class _Front extends \IPS\Dispatcher\Standard
}
}
/* Do things if we're actively using the easy theme editor */
\IPS\Theme::easyModePreOutput();
/* Meta tags */
\IPS\Output::i()->buildMetaTags();
/* Check MFA */
$this->checkMfa();
/* Check Alerts */
$this->checkAlerts();
/* Finish */
parent::finish();
}
/**
* Check MFA to see if we need to supply a code
* Check MFA to see if we need to supply a code. If the member elected to cancel, cancel (and redirect) here
*
* @param boolean $return Return any HTML (true) or add to Output (false)
*
@@ -685,6 +640,37 @@ class _Front extends \IPS\Dispatcher\Standard
$device = \IPS\Member\Device::loadOrCreate( $member, FALSE );
if ( $output = \IPS\MFA\MFAHandler::accessToArea( 'core', $device->known ? 'AuthenticateFrontKnown' : 'AuthenticateFront', \IPS\Request::i()->url(), $member ) )
{
/* Did we just cancel? */
if ( \IPS\Request::i()->_mfaCancel and ( ! \IPS\Member::loggedIn()->member_id or ( \IPS\Member::loggedIn()->member_id === $member->member_id ) ) )
{
/* We don't need this until we re-enter the MFA flow again */
unset( $_SESSION['processing2FA'] );
/* Is MFA required for this member? */
$mfaRequired = \IPS\Settings::i()->mfa_required_groups === '*' or $member->inGroup( explode( ',', \IPS\Settings::i()->mfa_required_groups ) );
/* Does this member require MFA upon login? */
$logout = \IPS\Settings::i()->mfa_required_prompt === 'immediate' and $mfaRequired;
/* Can they see this page without MFA? */
if ( !$mfaRequired OR ( $logout and $this->application->allowGuestAccess( $this->module, $this->controller, \IPS\Request::i()->do ) ) )
{
$redirectUrl = \IPS\Request::i()->url()->stripQueryString([ '_mfaCancel', '_mfaLogin', '_fromLogin', 'csrfKey' ]);
}
else
{
$redirectUrl = \IPS\Http\Url::internal( '' );
}
if ( $logout )
{
\IPS\Login::logout( $redirectUrl );
$redirectUrl = $redirectUrl->setQueryString( '_fromLogout', 1 );
}
\IPS\Output::i()->redirect( $redirectUrl );
}
if ( $return )
{
return $output;
@@ -699,6 +685,103 @@ class _Front extends \IPS\Dispatcher\Standard
}
}
/**
* Show a robots.txt file if configured to do so
*
* @param string $pathFromBaseUrl
*/
public function customResponse( string $pathFromBaseUrl )
{
if ( $pathFromBaseUrl === 'robots.txt' )
{
$this->robotsTxt();
}
else if ( \IPS\core\IndexNow::i()->isEnabled() AND $pathFromBaseUrl === \IPS\core\IndexNow::i()->getKeyFileName() )
{
$this->indexNow();
}
}
/**
* Return the IndexNow key.
*
* @return mixed
*/
protected function indexNow()
{
\IPS\Output::i()->sendOutput( \IPS\core\IndexNow::i()->getKeyfileContent(), 200, 'text/plain' );
}
/**
* Return the robots.txt files
*
* @return mixed
*/
protected function robotsTxt()
{
if ( \IPS\Settings::i()->robots_txt == 'default' )
{
\IPS\Output::i()->sendOutput( static::robotsTxtRules(), 200, 'text/plain' );
}
else if ( \IPS\Settings::i()->robots_txt != 'off' )
{
\IPS\Output::i()->sendOutput( \IPS\Settings::i()->robots_txt, 200, 'text/plain' );
}
throw new \OutOfRangeException;
}
/**
* Return the text for the robots.txt file
*
* @return string
*/
public static function robotsTxtRules(): string
{
$path = str_replace( '//', '/', '/' . trim( str_replace( 'robots.txt', '', \IPS\Http\Url::createFromString( \IPS\Http\Url::baseUrl() )->data[ \IPS\Http\Url::COMPONENT_PATH ] ), '/' ) . '/' );
$content = <<<FILE
# Rules for Invision Community (https://invisioncommunity.com)
User-Agent: *
# Block pages with no unique content
Disallow: {$path}startTopic/
Disallow: {$path}discover/unread/
Disallow: {$path}markallread/
Disallow: {$path}staff/
Disallow: {$path}online/
Disallow: {$path}discover/
Disallow: {$path}leaderboard/
Disallow: {$path}search/
Disallow: {$path}*?advancedSearchForm=
Disallow: {$path}register/
Disallow: {$path}lostpassword/
Disallow: {$path}login/
# Block faceted pages and 301 redirect pages
Disallow: {$path}*?sortby=
Disallow: {$path}*?filter=
Disallow: {$path}*?tab=
Disallow: {$path}*?do=
Disallow: {$path}*ref=
Disallow: {$path}*?forumId*
# Block profile pages as these have little unique value, consume a lot of crawl time and contain hundreds of 301 links
Disallow: {$path}profile/
FILE;
if ( \IPS\Settings::i()->sitemap_url )
{
$siteMapUrl = ( \IPS\Settings::i()->sitemap_url ) ? rtrim( \IPS\Settings::i()->sitemap_url, '/' ) : rtrim( \IPS\Settings::i()->base_url, '/' ) . '/sitemap.php';
$content .= <<<FILE
# Sitemap URL
Sitemap: {$siteMapUrl}
FILE;
}
return $content;
}
/**
* Output the basic javascript files every page needs
*
@@ -712,6 +795,10 @@ class _Front extends \IPS\Dispatcher\Standard
if ( !\IPS\Request::i()->isAjax() )
{
\IPS\Output::i()->globalControllers[] = 'core.front.core.app';
if ( \IPS\Settings::i()->core_datalayer_enabled )
{
\IPS\Output::i()->globalControllers[] = 'core.front.core.dataLayer';
}
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'front.js' ) );
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'front_core.js', 'core', 'front' ) );
@@ -777,4 +864,48 @@ class _Front extends \IPS\Dispatcher\Standard
}
}
}
/**
* Do Member Check
*
* @return \IPS\Http\Url|NULL
*/
protected static function doMemberCheck(): ?\IPS\Http\Url
{
foreach( \IPS\Application::applications() AS $app )
{
if ( $url = $app->doMemberCheck() )
{
return $url;
}
}
return NULL;
}
/**
*
*
* @return void
*/
public function checkAlerts()
{
/* Don't get in the way of validating members */
if ( \IPS\Member::loggedIn()->members_bitoptions['validating'] )
{
return;
}
/* Don't get in the way of the ModCP, registering, logging in, etc */
$ignoreControllers = [ 'modcp', 'register', 'login', 'redirect' ];
if( !\IPS\Request::i()->isAjax() and !\in_array( $this->controller, $ignoreControllers ) AND $alert = \IPS\core\Alerts\Alert::getNextAlertForMember( \IPS\Member::loggedIn() ) )
{
$alert->viewed( \IPS\Member::loggedIn() );
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'styles/alerts.css', 'core', 'front' ) );
\IPS\Output::i()->output .= \IPS\Theme::i()->getTemplate( 'alerts', 'core', 'front' )->alertModal( $alert, $url = base64_encode( \IPS\Request::i()->url() ) );
}
return;
}
}
+42 -22
View File
@@ -66,6 +66,21 @@ abstract class _Email
* @brief The number of emails that can be sent in one "go"
*/
const MAX_EMAILS_PER_GO = \IPS\BULK_MAILS_PER_CYCLE;
/**
* @brief Use the NoWrapper wrapper (basic HTML container)
*/
const WRAPPER_NONE = 0;
/**
* @brief Use the full email wrapper
*/
const WRAPPER_USE = 1;
/**
* @brief A wrapper has already been applied to the email content ( used in MergeAndSend() )
*/
const WRAPPER_APPLIED = 2;
/* !Factory Constructors */
@@ -89,10 +104,6 @@ abstract class _Email
{
return 'IPS\Email\Outgoing\Smtp';
}
elseif ( \IPS\CIC2 )
{
return 'IPS\Cicloud\Email';
}
elseif ( \IPS\CIC )
{
return 'IPS\Email\Outgoing\IpsCic';
@@ -143,16 +154,16 @@ abstract class _Email
/**
* Initiate a new custom email based on raw email content.
*
* @param string $subject Subject
* @param string $htmlContent HTML Version
* @param string|NULL $plaintextContent Plaintext version. If not provided, one will be built automatically based off $htmlContent.
* @param string $type See TYPE_* constants.
* @param bool $useWrapper If TRUE, the email will be wrapped in the default wrapper template
* @param string $emailKey Key used to identify the email, used for tracking purposes
* @param bool $trackingEnabled TRUE by default, pass FALSE to explicitly disable log/view tracking. Useful with mergeAndSend()'s default behavior of rebuilding an already built email
* @param string $subject Subject
* @param string $htmlContent HTML Version
* @param string|NULL $plaintextContent Plaintext version. If not provided, one will be built automatically based off $htmlContent.
* @param string $type See TYPE_* constants.
* @param int $useWrapper See WRAPPER_* constants.
* @param string $emailKey Key used to identify the email, used for tracking purposes
* @param bool $trackingEnabled TRUE by default, pass FALSE to explicitly disable log/view tracking. Useful with mergeAndSend()'s default behavior of rebuilding an already built email
* @return \IPS\Email
*/
public static function buildFromContent( $subject, $htmlContent='', $plaintextContent=NULL, $type = NULL, $useWrapper=TRUE, $emailKey=NULL, $trackingEnabled=TRUE )
public static function buildFromContent( $subject, $htmlContent='', $plaintextContent=NULL, $type = NULL, $useWrapper=\IPS\Email::WRAPPER_USE, $emailKey=NULL, $trackingEnabled=TRUE )
{
if( !$type )
{
@@ -167,6 +178,12 @@ abstract class _Email
}
}
if( \IPS\IN_DEV AND \is_bool( $useWrapper ) )
{
trigger_error( "The email wrapper must be passed a constant when calling buildFromContent()", E_USER_ERROR );
exit;
}
$email = static::factory( $type );
$email->type = $type;
$email->subject = $subject;
@@ -242,7 +259,7 @@ abstract class _Email
/**
* @brief Should the default wrapper template be used?
*/
protected $useWrapper = TRUE;
protected $useWrapper = self::WRAPPER_USE;
/**
* @brief Unsubscribe Template App
@@ -349,9 +366,12 @@ abstract class _Email
{
$return = static::template( $this->templateApp, $this->templateKey, $type, array_merge( $this->templateParams, array( $this ) ) );
}
/* Check whether we have a subject yet, if not generate it */
$subject = $this->subject ?? $this->compileSubject( $member ?: NULL, $language );
/* Wrap in the wrapper if necessary */
if ( $this->useWrapper )
if ( $this->useWrapper == static::WRAPPER_USE )
{
/* Compile the unsubscribe link */
$unsubscribe = '';
@@ -368,11 +388,11 @@ abstract class _Email
}
/* Wrap */
$return = static::template( 'core', 'emailWrapper', $type, array( $this->subject, $member ?: new \IPS\Member, $return, $unsubscribe, $member === FALSE, '', $this, $ourPicks ) );
$return = static::template( 'core', 'emailWrapper', $type, array( $subject, $member ?: new \IPS\Member, $return, $unsubscribe, $member === FALSE, '', $this, $ourPicks ) );
}
else
elseif( $this->useWrapper == static::WRAPPER_NONE )
{
$return = static::template( 'core', 'emailNoWrapper', $type, array( $this->subject, $member ?: new \IPS\Member, $return, '', $member === FALSE, '', $this, NULL ) );
$return = static::template( 'core', 'emailNoWrapper', $type, array( $subject, $member ?: new \IPS\Member, $return, '', $member === FALSE, '', $this, NULL ) );
}
/* Parse language */
@@ -484,7 +504,7 @@ abstract class _Email
/**
* Get subject
*
* @param \IPS\Member|NULL|FALSE $member If the email is going to a member, the member object. Ensures correct language is used and the email starts with "Hi {member}". NULL for no member, FALSE to use "Hi *|member_name|*" for mergeAndSend()
* @param \IPS\Member|NULL $member If the email is going to a member, the member object. Ensures correct language is used and the email starts with "Hi {member}". NULL for no member, FALSE to use "Hi *|member_name|*" for mergeAndSend()
* @param \IPS\Lang|NULL $language If provided, will override the $member language
* @return string
*/
@@ -712,7 +732,7 @@ abstract class _Email
/* Sent successfully, remove notification */
\IPS\core\AdminNotification::remove( 'core', 'ConfigurationError', 'failedMail' );
\IPS\Db::i()->update( 'core_mail_error_logs', array( 'mlog_notification_sent' => TRUE ) );
\IPS\Db::i()->update( 'core_mail_error_logs', [ 'mlog_notification_sent' => TRUE ], [ 'mlog_notification_sent=?', 0 ] );
/* Update the ad impression count if appropriate */
if( $updateAds === TRUE )
@@ -824,7 +844,7 @@ abstract class _Email
* @param mixed $fromEmail The email address to send from. If NULL, default setting is used. NOTE: This should always be a site-controlled domin. Some services like Sparkpost require the domain to be validated.
* @param mixed $fromName The name the email should appear from. If NULL, default setting is used
* @param array $additionalHeaders Additional headers to send. Merge tags can be used like in content.
* @param \IPS\Lang $language The language the email content should be in
* @param \IPS\Lang|NULL $language The language the email content should be in
* @return int Number of successful sends
*/
public function mergeAndSend( $recipients, $fromEmail = NULL, $fromName = NULL, $additionalHeaders = array(), \IPS\Lang $language = NULL )
@@ -863,7 +883,7 @@ abstract class _Email
}
}
if ( static::buildFromContent( $subject, $htmlContent, $plaintextContent, $this->type, FALSE, $this->templateKey, FALSE )->send( $address, array(), array(), $fromEmail, $fromName, $_additionalHeaders, TRUE, FALSE ) )
if ( static::buildFromContent( $subject, $htmlContent, $plaintextContent, $this->type, static::WRAPPER_APPLIED, $this->templateKey, FALSE )->send( $address, array(), array(), $fromEmail, $fromName, $_additionalHeaders, TRUE, FALSE ) )
{
$return++;
}
+1 -2
View File
@@ -809,8 +809,7 @@ class _Email
}
/* If any of these headers are present with any values, ignore the email */
if( !empty( $this->headers['x-auto-response-suppress'] ) OR
!empty( $this->headers['x-autorespond'] ) OR
if( !empty( $this->headers['x-autorespond'] ) OR
!empty( $this->headers['x-autoreply'] ) OR
!empty( $this->headers['x-autoreply-From'] ) OR
!empty( $this->headers['x-mail-autoreply'] )
+3 -3
View File
@@ -54,15 +54,15 @@ class _Debug extends \IPS\Email
{
if( !is_dir( $this->debugPath ) )
{
throw new \IPS\Email\Outgoing\Exception( 'no_path_email_debug', 0, NULL, array( $this->debugPath ) );
throw new \IPS\Email\Outgoing\Exception( 'no_path_email_debug', 1, NULL, array( $this->debugPath ) );
}
$fullEmailContents = $this->compileFullEmail( $to, $cc, $bcc, $fromEmail, $fromName, $additionalHeaders );
$filename = date("M-j-Y") . '-' . microtime() . '-' . urlencode( mb_substr( $this->_parseRecipients( $to, TRUE ), 0, 200 ) ) . ".eml";
if( !@\file_put_contents( rtrim( $this->debugPath, '/' ) . '/' . $filename, $fullEmailContents ) )
{
throw new \IPS\Email\Outgoing\Exception( 'no_write_email_debug', 0, NULL, array( rtrim( $this->debugPath, '/' ) . '/' . $filename ) );
throw new \IPS\Email\Outgoing\Exception( 'no_write_email_debug', 2, NULL, array( rtrim( $this->debugPath, '/' ) . '/' . $filename ) );
}
}
}
+7
View File
@@ -40,6 +40,13 @@ class _Exception extends \RuntimeException
{
/* Store these for the extraLogData() method */
$this->extraDetails = $extra;
switch ( $code )
{
default:
$message = \IPS\Member::loggedIn()->language()->addToStack( $message, FALSE, array( 'sprintf' => $extra ) );
break;
}
return parent::__construct( $message, $code, $previous );
}
+7 -2
View File
@@ -35,7 +35,12 @@ class _PHP extends \IPS\Email
* @throws \IPS\Email\Outgoing\Exception
*/
public function _send( $to, $cc=array(), $bcc=array(), $fromEmail = NULL, $fromName = NULL, $additionalHeaders = array() )
{
{
if( !\function_exists( 'mail' ) )
{
throw new \IPS\Email\Outgoing\Exception( 'email_test_mailfunction_disabled' );
}
$boundary = "--==_mimepart_" . md5( mt_rand() );
$subject = $this->compileSubject( static::_getMemberFromRecipients( $to ) );
@@ -50,7 +55,7 @@ class _PHP extends \IPS\Email
try
{
if ( !mail( $this->_parseRecipients( $to, TRUE ), static::encodeHeader( $subject ), $this->_compileMessage( static::_getMemberFromRecipients( $to ), $boundary, PHP_EOL, 68 ), implode( PHP_EOL, $headers ), \IPS\Settings::i()->php_mail_extra ) )
if ( !mail( $this->_parseRecipients( $to, TRUE ), static::encodeHeader( $subject ), $this->_compileMessage( static::_getMemberFromRecipients( $to ), $boundary, "\r\n", 68 ), implode( "\r\n", $headers ), \IPS\Settings::i()->php_mail_extra ) )
{
if ( $error = \IPS\IPS::$lastError )
{
+2 -2
View File
@@ -124,10 +124,10 @@ class _SendGrid extends \IPS\Email
* @param mixed $fromEmail The email address to send from. If NULL, default setting is used. NOTE: This should always be a site-controlled domin. Some services like Sparkpost require the domain to be validated.
* @param mixed $fromName The name the email should appear from. If NULL, default setting is used
* @param array $additionalHeaders Additional headers to send. Merge tags can be used like in content.
* @param \IPS|Lang $language The language the email content should be in
* @param \IPS\Lang|NULL $language The language the email content should be in
* @return int Number of successful sends
*/
public function mergeAndSend( $recipients, $fromEmail = NULL, $fromName = NULL, $additionalHeaders = array(), \IPS\Lang $language )
public function mergeAndSend( $recipients, $fromEmail = NULL, $fromName = NULL, $additionalHeaders = array(), \IPS\Lang $language = NULL )
{
/* Initiate the request */
$request = $this->_initRequest( $fromEmail, $fromName );
+12
View File
@@ -34,6 +34,18 @@ class _Database extends \IPS\File
{
return array();
}
/**
* Test Settings
*
* @param array $values The submitted values
* @return void
* @throws \LogicException
*/
public static function testSettings( &$values )
{
}
/**
* Display name
+102 -26
View File
@@ -51,13 +51,18 @@ abstract class _File
/**
* @brief File extensions considered safe. Not an exhaustive list but these are the files we're most interested in being recognised.
*/
public static $safeFileExtensions = array( 'js', 'css', 'txt', 'ico', 'gif', 'jpg', 'jpe', 'jpeg', 'png', 'mp4', '3gp', 'mov', 'ogg', 'ogv', 'mp3', 'mpg', 'mpeg', 'ico', 'flv', 'webm', 'wmv', 'avi', 'm4v' );
public static $safeFileExtensions = array( 'js', 'css', 'txt', 'ico', 'gif', 'jpg', 'jpe', 'jpeg', 'png', 'mp4', '3gp', 'mov', 'ogg', 'ogv', 'mp3', 'mpg', 'mpeg', 'ico', 'flv', 'webm', 'wmv', 'avi', 'm4v', 'webp', 'm4a' );
/**
* @brief File extensions for HTML5 compatible videos
*/
public static $videoExtensions = array( 'mp4', '3gp', 'mov', 'ogg', 'ogv', 'mpg', 'mpeg', 'flv', 'webm', 'wmv', 'avi', 'm4v' );
/**
* @brief File extensions for HTML5 compatible audio
*/
public static $audioExtensions = array( 'mp3', 'ogg', 'wav', 'm4a' );
/**
* @brief Does this storage method support chunked uploads?
*/
@@ -385,7 +390,7 @@ abstract class _File
/* Now loop over each file */
$currentTotal = 0;
foreach( $files as $file )
foreach( $files as $i => $file )
{
/* First, validate the upload */
try
@@ -426,13 +431,13 @@ abstract class _File
if( $callback !== NULL )
{
$contents = file_get_contents( $file['tmp_name'] );
$contents = $callback( $contents, $file['name'] );
$contents = $callback( $contents, $file['name'], $i );
$fileObjects[] = static::create( $storageLocation, $file['name'], $contents, $container, FALSE, NULL, $obscure );
$fileObjects[ $i ] = static::create( $storageLocation, $file['name'], $contents, $container, FALSE, NULL, $obscure );
}
else
{
$fileObjects[] = static::create( $storageLocation, $file['name'], NULL, $container, FALSE, $file['tmp_name'], $obscure );
$fileObjects[ $i ] = static::create( $storageLocation, $file['name'], NULL, $container, FALSE, $file['tmp_name'], $obscure );
}
if( is_file( $file['tmp_name'] ) and file_exists( $file['tmp_name'] ) )
@@ -745,6 +750,11 @@ abstract class _File
*/
public $tempId;
/**
* @brief A flag used immediately after the file is uploaded to indicate if it has been flagged by the image scanner
*/
public $requiresModeration = NULL;
/**
* Constructor
*
@@ -983,7 +993,7 @@ abstract class _File
$filename = str_replace( '.' . $matches[2], '_' . $matches[2], $filename );
}
return str_replace( array( ' ', '#' ), '_', $filename ) . '.' . md5( mt_rand() ) . ( ( $safe ) ? '.' . $ext : '' );
return str_replace( array( ' ', '#' ), '_', $filename ) . '.' . \IPS\Login::generateRandomString( 32 ) . ( ( $safe ) ? '.' . $ext : '' );
}
/**
@@ -997,7 +1007,7 @@ abstract class _File
$ext = mb_substr( $filename, ( mb_strrpos( $filename, '.' ) + 1 ) );
if ( mb_strlen( $ext ) == 32 )
{
preg_match( '#(.*)_([A-Z0-9\-]{2,10})\.([A-Z0-9]{32})#i', $filename, $matches );
preg_match( '#(.*)_([A-Z0-9\-]{2,14})\.([A-Z0-9]{32})#i', $filename, $matches );
if ( isset( $matches[1] ) and isset( $matches[2] ) )
{
@@ -1006,7 +1016,7 @@ abstract class _File
else
{
/* Fix foo.pdf.hash */
preg_match( '#(.*)\.([A-Z0-9\-]{2,10})\.([A-Z0-9]{32})#i', $filename, $matches );
preg_match( '#(.*)\.([A-Z0-9\-]{2,14})\.([A-Z0-9]{32})#i', $filename, $matches );
if ( isset( $matches[1] ) and isset( $matches[2] ) )
{
@@ -1287,12 +1297,14 @@ abstract class _File
/**
* Make into an attachment
*
* @param string $postKey Post key
* @param \IPS\Member|NULL $member Member who uploaded the attachment
* @param string $postKey Post key
* @param \IPS\Member|NULL $member Member who uploaded the attachment
* @param bool $requiresModeration If this attachment requires moderation
* @param array|NULL $labels Image scanner labels
* @return array
* @throws \DomainException
*/
public function makeAttachment( $postKey, \IPS\Member $member = NULL )
public function makeAttachment( $postKey, \IPS\Member $member = NULL, $requiresModeration = FALSE, $labels=NULL )
{
$ext = mb_substr( $this->originalFilename, mb_strrpos($this->originalFilename, '.') + 1 );
@@ -1301,21 +1313,22 @@ abstract class _File
$this->securityKey = \IPS\Login::generateRandomString();
$data = array(
'attach_ext' => $ext,
'attach_file' => $this->originalFilename,
'attach_location' => (string) $this,
'attach_thumb_location' => '',
'attach_thumb_width' => 0,
'attach_thumb_height' => 0,
'attach_is_image' => 0,
'attach_hits' => 0,
'attach_date' => time(),
'attach_post_key' => $postKey,
'attach_member_id' => $memberId,
'attach_filesize' => $this->filesize(),
'attach_img_width' => 0,
'attach_img_height' => 0,
'attach_is_archived' => FALSE,
'attach_ext' => $ext,
'attach_file' => $this->originalFilename,
'attach_location' => (string) $this,
'attach_thumb_location' => '',
'attach_thumb_width' => 0,
'attach_thumb_height' => 0,
'attach_is_image' => 0,
'attach_hits' => 0,
'attach_date' => time(),
'attach_post_key' => $postKey,
'attach_member_id' => $memberId,
'attach_filesize' => $this->filesize(),
'attach_img_width' => 0,
'attach_img_height' => 0,
'attach_is_archived' => FALSE,
'attach_moderation_status' => $requiresModeration ? 'pending' : 'skipped',
'attach_security_key' => $this->securityKey
);
@@ -1339,6 +1352,8 @@ abstract class _File
$this->attachmentThumbnailUrl = $data['attach_thumb_location'];
}
$data['attach_labels'] = $labels;
}
catch ( \InvalidArgumentException $e ) { }
@@ -1380,6 +1395,23 @@ abstract class _File
return FALSE;
}
/**
* Determine if the file is a video
*
* @return bool
*/
public function isAudio()
{
$ext = mb_substr( $this->originalFilename, mb_strrpos( $this->originalFilename, '.' ) + 1 );
if ( \in_array( mb_strtolower( $ext ), \IPS\File::$audioExtensions ) )
{
return TRUE;
}
return FALSE;
}
/**
* Get media type ("file", "image" or "video")
@@ -1396,6 +1428,10 @@ abstract class _File
{
return 'image';
}
elseif ( $this->isAudio() )
{
return 'audio';
}
else
{
return 'file';
@@ -1763,6 +1799,7 @@ abstract class _File
'ogg' => 'file-audio-o',
'ogv' => 'file-audio-o',
'wav' => 'file-audio-o',
'm4a' => 'file-audio-o',
'avi' => 'file-video-o',
'flv' => 'file-video-o',
'mkv' => 'file-video-o',
@@ -2692,4 +2729,43 @@ abstract class _File
'zmm' => 'application/vnd.handheld-entertainment+xml'
);
/**
* Is this an animated image ( does it contain multiple frames )?
*
* @return bool
*/
public function isAnimatedImage(): bool
{
$offset = 0;
$frame = 0;
$contents = $this->contents();
while ( $frame < 2 )
{
$pos = \strpos( $contents, "\x00\x21\xF9\x04", $offset );
if ( $pos === false )
{
break;
}
else
{
$offset = $pos + 1;
$newPos = \strpos( $contents, "\x00\x2C", $offset );
if ( $newPos === false )
{
break;
}
else
{
if ( $pos + 8 == $newPos )
{
$frame++;
}
$offset = $newPos + 1;
}
}
}
return $frame > 1;
}
}
+4 -1
View File
@@ -78,7 +78,10 @@ class _Ftp
*/
public function __destruct()
{
@ftp_close( $this->ftp );
if( $this->ftp !== NULL )
{
@ftp_close( $this->ftp );
}
}
/**
-1
View File
@@ -102,7 +102,6 @@ class _GeoLocation
'FJ', // Fiji
'FI', // Finland
'FR', // France
'FX', // France, Metropolitan
'GF', // French Guiana
'PF', // French Polynesia
'TF', // French Southern Territories
+79 -5
View File
@@ -61,6 +61,16 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
* @brief Table Column Formatters
*/
public $tableParsers = array();
/**
* @brief Along with the other series, show one of the sum of all
*/
public $allRecordsSeries = FALSE;
/**
* @brief The lang string used to label the 'all records' series
*/
public $allRecordsLang = 'database_chart_all_series';
/**
* @brief Custom form
@@ -182,7 +192,10 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
}
$this->groupBy = $this->customFiltersForm['groupBy'];
$this->where = array( array( $whereFunction( $values ) ) );
$whereFunctionResult = $whereFunction( $values );
$this->where[] = \is_array( $whereFunctionResult ) ? $whereFunctionResult : array( $whereFunctionResult );
foreach( $seriesFunction( $values ) as $series )
{
@@ -195,7 +208,9 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
$seriesFunction = $this->customFiltersForm['series'];
$this->groupBy = $this->customFiltersForm['groupBy'];
$this->where = array( array( $whereFunction( $this->savedCustomFilters ) ) );
$whereFunctionResult = $whereFunction( $this->savedCustomFilters );
$this->where[] = \is_array( $whereFunctionResult ) ? $whereFunctionResult : array( $whereFunctionResult );
foreach( $seriesFunction( $this->savedCustomFilters ) as $series )
{
@@ -212,7 +227,65 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
}
}
}
if ( $this->allRecordsSeries )
{
/* Work out where clause */
$where = $this->where;
$where[] = array( "{$this->dateField}>?", 0 );
if ( $this->start )
{
$where[] = array( "{$this->dateField}>?", $this->start->getTimestamp() );
}
if ( $this->end )
{
$where[] = array( "{$this->dateField}<?", $this->end->getTimestamp() );
}
/* What's our SQL time? */
switch ( $this->timescale )
{
case 'hourly':
$timescale = '%Y-%c-%e-%H-%i-%s';
break;
case 'daily':
$timescale = '%Y-%c-%e';
break;
case 'weekly':
$timescale = '%x-%v';
break;
case 'monthly':
$timescale = '%Y-%c';
break;
}
$fromUnixTime = "FROM_UNIXTIME( IFNULL( {$this->dateField}, 0 ) )";
if ( !$this->timezoneError and \IPS\Member::loggedIn()->timezone and \in_array( \IPS\Member::loggedIn()->timezone, \DateTimeZone::listIdentifiers() ) )
{
$fromUnixTime = "CONVERT_TZ( {$fromUnixTime}, @@session.time_zone, '" . \IPS\Db::i()->escape_string( \IPS\Member::loggedIn()->timezone ) . "' )";
}
$allRecordsStatement = \IPS\Db::i()->select(
"SQL_BIG_RESULT DATE_FORMAT( {$fromUnixTime}, '{$timescale}' ) AS time_as_alias, COUNT(*) as $this->allRecordsLang",
$this->table,
$where,
'time_as_alias ASC',
NULL,
'time_as_alias'
);
foreach ( $allRecordsStatement as $allRecordsRow )
{
$data[ $allRecordsRow['time_as_alias'] ] = $data[ $allRecordsRow['time_as_alias'] ] ?? array();
$data[ $allRecordsRow['time_as_alias'] ][$this->allRecordsLang] = $allRecordsRow[$this->allRecordsLang];
}
$this->addSeries( \IPS\Member::loggedIn()->language()->addToStack($this->allRecordsLang), 'number', 'COUNT(*)', FALSE, $this->allRecordsLang );
}
return $data;
}
@@ -303,7 +376,7 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
}
$stmt = \IPS\Db::i()->select(
"SQL_BIG_RESULT DATE_FORMAT( {$fromUnixTime}, '{$timescale}' ) AS time_as_alias, " . implode( ', ', array_unique( $this->series ) ) . ( $this->groupBy ? ", " . $this->groupBy : '' ),
"SQL_BIG_RESULT DATE_FORMAT( {$fromUnixTime}, '{$timescale}' ) AS time_as_alias" . ( \count( $this->series ) ? "," : "" ) . implode( ', ', array_unique( $this->series ) ) . ( $this->groupBy ? ", " . $this->groupBy : '' ),
$this->table,
$where,
'time_as_alias ASC',
@@ -330,7 +403,7 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
continue;
}
}
foreach( $this->series as $column )
{
if( $this->groupBy )
@@ -546,6 +619,7 @@ class _Database extends \IPS\Helpers\Chart\Dynamic
}
$this->compileForOutput();
if ( isset( \IPS\Request::i()->download ) )
{
return $this->download();
+2 -1
View File
@@ -151,8 +151,9 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
/* If we are deleting a chart, just do that now and redirect */
if( isset( \IPS\Request::i()->deleteChart ) )
{
\IPS\Session::i()->csrfCheck();
\IPS\Db::i()->delete( 'core_saved_charts', array( 'chart_id=? and chart_member=?', \IPS\Request::i()->deleteChart, \IPS\Member::loggedIn()->member_id ) );
\IPS\Output::i()->redirect( \IPS\Request::i()->url()->stripQueryString( array( 'deleteChart', 'chartId' ) ), 'chart_deleted' );
\IPS\Output::i()->redirect( \IPS\Request::i()->url()->stripQueryString( array( 'deleteChart', 'chartId', 'csrfKey' ) ), 'chart_deleted' );
}
if ( !isset( $options['chartArea'] ) )
@@ -0,0 +1,39 @@
<?php
/**
* @brief Invisible captcha Interface
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @since 10 Mai 2022
*/
namespace IPS\Helpers\Form\Captcha;
/* 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;
}
/**
* captcha Interface
*/
interface CaptchaInterface
{
/**
* Display
*
* @return string
*/
public function getHtml();
/**
* Verify
*
* @return bool|null TRUE/FALSE indicate if the test passed or not. NULL indicates the test failed, but the captcha system will display an error so we don't have to.
*/
public function verify();
}
+88
View File
@@ -0,0 +1,88 @@
<?php
/**
* @brief hCaptcha
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @since 10 Mai 2022
*/
namespace IPS\Helpers\Form\Captcha;
/* 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;
}
/**
* hCaptcha
*/
class _Hcaptcha implements CaptchaInterface
{
/**
* Does this CAPTCHA service support being added in a modal?
*/
public static $supportsModal = FALSE;
/**
* @brief Error
*/
protected $error;
/**
* Display
*
* @return string
*/
public function getHtml()
{
\IPS\Output::i()->jsFilesAsync[] = "https://js.hcaptcha.com/1/api.js?hl=" . \IPS\Member::loggedIn()->language()->short ;
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->hCaptcha( \IPS\Settings::i()->hcaptcha_sitekey, preg_replace( '/^(.+?)\..*$/', '$1', \IPS\Member::loggedIn()->language()->short ) );
}
/**
* Display
*
* @return string
*/
public function rowHtml()
{
\IPS\Output::i()->jsFilesAsync[] = "https://js.hcaptcha.com/1/api.js?hl=" . \IPS\Member::loggedIn()->language()->short ;
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->hCaptcha( \IPS\Settings::i()->hcaptcha_sitekey, preg_replace( '/^(.+?)\..*$/', '$1', \IPS\Member::loggedIn()->language()->short ) );
}
/**
* Verify
*
* @return bool|null TRUE/FALSE indicate if the test passed or not. NULL indicates the test failed, but the captcha system will display an error so we don't have to.
*/
public function verify()
{
try
{
$response = \IPS\Http\Url::external( 'https://hcaptcha.com/siteverify' )->request()->post( array(
'secret' => \IPS\Settings::i()->hcaptcha_secret,
'response' => trim( \IPS\Request::i()->__get('h-captcha-response') ),
'remoteip' => \IPS\Request::i()->ipAddress(),
) )->decodeJson( TRUE );
$hostname = \IPS\Http\Url::internal('')->data[ \IPS\Http\Url::COMPONENT_HOST ];
return ( ( (bool) $response['success'] ) and ( $response['hostname'] === mb_substr( $hostname, \mb_strlen( $hostname ) - mb_strlen( $response['hostname'] ) ) ) );
}
catch( \RuntimeException $e )
{
if( $e->getMessage() == 'BAD_JSON' )
{
return FALSE;
}
else
{
throw $e;
}
}
}
}
+1 -1
View File
@@ -20,7 +20,7 @@ if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
/**
* Invisible reCAPTCHA
*/
class _Invisible
class _Invisible implements CaptchaInterface
{
/**
* Does this CAPTCHA service support being added in a modal?
+1 -1
View File
@@ -20,7 +20,7 @@ if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
/**
* keyCAPTCHA
*/
class _Keycaptcha
class _Keycaptcha implements CaptchaInterface
{
/**
* Does this CAPTCHA service support being added in a modal?
+1 -1
View File
@@ -20,7 +20,7 @@ if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
/**
* reCAPTCHA
*/
class _Recaptcha2
class _Recaptcha2 implements CaptchaInterface
{
/**
* Does this CAPTCHA service support being added in a modal?
+3 -3
View File
@@ -38,9 +38,9 @@ class _CheckboxSet extends Select
public function __construct( $name, $defaultValue=NULL, $required=FALSE, $options=array(), $customValidationCode=NULL, $prefix=NULL, $suffix=NULL, $id=NULL )
{
$options['multiple'] = TRUE;
$options['showAllNone'] = $options['showAllNone'] ?? TRUE;
parent::__construct( $name, $defaultValue, $required, $options, $customValidationCode, $prefix, $suffix, $id );
if ( !isset( $this->options['descriptions'] ) )
{
$this->options['descriptions'] = array();
@@ -93,7 +93,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'] );
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'] );
}
/**
+1 -1
View File
@@ -179,7 +179,7 @@ class _Date extends FormAbstract
{
if ( \is_int( $v ) or ( $v and \is_string( $v ) and preg_match( '/^[0-9]+$/', $v ) ) )
{
return \IPS\DateTime::ts( $v, $timezone );
return \IPS\DateTime::ts( $v );
}
else if( \is_array( $v ) )
{
+101 -11
View File
@@ -66,7 +66,8 @@ class _Editor extends FormAbstract
'maxLength' => 16777215, // The default for MEDIUMTEXT */
'editorId' => NULL,
'allowAttachments' => TRUE,
'ipsPlugins' => "ipsautolink,ipsautosave,ipsctrlenter,ipscode,ipscontextmenu,ipsemoticon,ipsimage,ipslink,ipsmentions,ipspage,ipspaste,ipspreview,ipsquote,ipsspoiler,ipsautogrow,ipssource,removeformat"
'ipsPlugins' => "ipsautolink,ipsautosave,ipsctrlenter,ipscode,ipscontextmenu,ipsemoticon,ipsimage,ipslink,ipsmentions,ipspage,ipspaste,ipsquote,ipsspoiler,ipsautogrow,ipssource,removeformat",
'profanityBlock' => TRUE,
);
/**
@@ -103,7 +104,34 @@ class _Editor extends FormAbstract
if ( \IPS\Settings::i()->giphy_enabled )
{
$this->defaultOptions['ipsPlugins'] .= ',ipsgiphy';
$this->defaultOptions['ipsPlugins'] .= ',ipsgiphy';
}
if( \IPS\Dispatcher::hasInstance() and ( \IPS\Dispatcher::i()->controllerLocation == 'front' OR ( \IPS\Dispatcher::i()->controllerLocation == 'admin' AND \IPS\Dispatcher::i()->module->key === 'editor' AND \IPS\Dispatcher::i()->controller == 'toolbar' )))
{
$this->defaultOptions['ipsPlugins'] .= ',ipspreview';
}
if ( isset( $options['allButtons'] ) AND $options['allButtons'] )
{
$showStockReplies = TRUE;
}
else
{
$showStockReplies = FALSE;
foreach ( \IPS\core\StoredReplies::getStore() as $reply )
{
$reply = \IPS\core\StoredReplies::constructFromData( $reply );
if ( $reply->enabled and $reply->can( 'view' ) )
{
$showStockReplies = TRUE;
}
}
}
if ( $showStockReplies )
{
$this->defaultOptions['ipsPlugins'] .= ',ipsstockreplies';
}
if ( isset( \IPS\Request::i()->usingEditor ) and \IPS\Request::i()->isAjax() and \IPS\Dispatcher::hasInstance() and \IPS\Dispatcher::i()->controllerLocation == 'front' )
@@ -309,6 +337,23 @@ class _Editor extends FormAbstract
$attachmentArea = \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->editorAttachmentsPlaceholder( $this->name, $this->postKey, $this->noUploaderError, $this->canUseMediaExtension() );
}
if ( \IPS\Member::loggedIn()->group['g_bypass_badwords'] )
{
$this->options['profanityBlock'] = FALSE;
}
if ( $this->options['profanityBlock'] )
{
$this->options['profanityBlock'] = [];
foreach( \IPS\core\Profanity::getProfanity() AS $profanity )
{
if ( $profanity->action == 'block' )
{
$this->options['profanityBlock'][] = [ 'word' => $profanity->type, 'type' => $profanity->m_exact ? 'exact' : 'loose' ];
}
}
}
/* Display */
$template = $raw ? 'editorRaw' : 'editor';
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->$template( $this->name, $value, $this->options, $allowed, md5( $this->options['autoSaveKey'] . ':' . session_id() ), $attachmentArea, json_encode( array() ), $this->options['tags'] );
@@ -339,7 +384,12 @@ class _Editor extends FormAbstract
return $value;
}
/**
* @brief Save on queries and fetch the alt label would just the once
*/
protected $_altLabelWord = NULL;
/**
* Get Value
*
@@ -381,9 +431,22 @@ class _Editor extends FormAbstract
if ( !isset( $parser ) or !\in_array( $attachment['attach_id'], $parser->mappedAttachments ) or array_key_exists( $attachment['attach_id'], $parser->existingAttachments ) )
{
$ext = mb_substr( $attachment['attach_file'], mb_strrpos( $attachment['attach_file'], '.' ) + 1 );
if ( \in_array( mb_strtolower( $ext ), \IPS\File::$videoExtensions ) )
if ( \in_array( mb_strtolower( $ext ), \IPS\File::$videoExtensions ) or \in_array( mb_strtolower( $ext ), \IPS\File::$audioExtensions ) )
{
$value .= \IPS\Theme::i()->getTemplate( 'editor', 'core', 'global' )->attachedVideo( $attachment['attach_location'], \IPS\Http\Url::baseUrl( \IPS\Http\Url::PROTOCOL_RELATIVE ) . "applications/core/interface/file/attachment.php?id=" . $attachment['attach_id'], $attachment['attach_file'], \IPS\File::getMimeType( $attachment['attach_file'] ), $attachment['attach_id'] );
$url = \IPS\Http\Url::baseUrl( \IPS\Http\Url::PROTOCOL_RELATIVE ) . "applications/core/interface/file/attachment.php?id=" . $attachment['attach_id'];
if ( $attachment['attach_security_key'] )
{
$url .= "&key={$attachment['attach_security_key']}";
}
if ( \in_array( mb_strtolower( $ext ), \IPS\File::$videoExtensions ) )
{
$value .= \IPS\Theme::i()->getTemplate( 'editor', 'core', 'global' )->attachedVideo( $attachment['attach_location'], $url, $attachment['attach_file'], \IPS\File::getMimeType( $attachment['attach_file'] ), $attachment['attach_id'] );
}
elseif ( \in_array( mb_strtolower( $ext ), \IPS\File::$audioExtensions ) )
{
$value .= \IPS\Theme::i()->getTemplate( 'editor', 'core', 'global' )->attachedAudio( $attachment['attach_location'], $url, $attachment['attach_file'], \IPS\File::getMimeType( $attachment['attach_file'] ), $attachment['attach_id'] );
}
}
elseif ( $attachment['attach_is_image'] )
{
@@ -397,8 +460,21 @@ class _Editor extends FormAbstract
$ratio = round( ( $attachment['attach_img_height'] / $attachment['attach_img_width'] ) * 100, 2 );
$width = $attachment['attach_img_width'];
}
$value .= \IPS\Theme::i()->getTemplate( 'editor', 'core', 'global' )->attachedImage( $attachment['attach_location'], $attachment['attach_thumb_location'] ? $attachment['attach_thumb_location'] : $attachment['attach_location'], $attachment['attach_file'], $attachment['attach_id'], $width, $ratio );
$altText = NULL;
if ( ! empty( $attachment['attach_labels'] ) )
{
if ( $this->_altLabelWord === NULL )
{
/* This is stored with the post, so it cannot be the user's language */
$this->_altLabelWord = \IPS\Lang::load( \IPS\Lang::defaultLanguage() )->get( 'alt_label_could_be' );
}
$altText = $this->_altLabelWord . ' ' . implode( ', ', \IPS\Text\Parser::getAttachmentLabels( $attachment ) );
}
$value .= \IPS\Theme::i()->getTemplate( 'editor', 'core', 'global' )->attachedImage( $attachment['attach_location'], $attachment['attach_thumb_location'] ? $attachment['attach_thumb_location'] : $attachment['attach_location'], $attachment['attach_file'], $attachment['attach_id'], $width, $ratio, $altText );
}
else
{
@@ -409,7 +485,7 @@ class _Editor extends FormAbstract
}
$fileAttachments[] = \IPS\Theme::i()->getTemplate( 'editor', 'core', 'global' )->attachedFile( $url, $attachment['attach_file'], FALSE, $attachment['attach_ext'], $attachment['attach_id'], $attachment['attach_security_key'] );
}
if ( !isset( $parser ) or !\in_array( $attachment['attach_id'], $parser->mappedAttachments ) )
{
$inserts[] = array(
@@ -435,7 +511,7 @@ class _Editor extends FormAbstract
{
\IPS\Db::i()->insert( 'core_attachments_map', $inserts, TRUE );
}
/* Clear out the post key for attachments that are claimed automatically */
if ( $this->options['attachIds'] )
{
@@ -457,7 +533,7 @@ class _Editor extends FormAbstract
}
catch ( \Exception $e ) { }
}
/* Throw any errors */
if ( $error )
{
@@ -638,10 +714,24 @@ class _Editor extends FormAbstract
'totalMaxSize' => $maxTotalSize,
'maxFileSize' => \IPS\Member::loggedIn()->group['g_attach_per_post'] ? ( \IPS\Member::loggedIn()->group['g_attach_per_post'] * 1024 ) / 1048576 : NULL,
'allowStockPhotos' => $allowStockPhotos,
'canBeModerated' => ($this->extension AND method_exists( $this->extension, 'canBeModerated' ) and $this->extension->canBeModerated( \IPS\Member::loggedIn(), $this ) ),
'callback' => function( $file ) use ( $postKey )
{
try
{
$fileInfo = \IPS\Db::i()->select( 'requires_moderation, labels', 'core_files_temp', array( 'contents=?', (string) $file ), NULL, NULL, NULL, NULL, \IPS\Db::SELECT_FROM_WRITE_SERVER )->first();
$requiresModeration = (bool) $fileInfo['requires_moderation'];
$labels = $fileInfo['labels'];
}
catch ( \UnderflowException $e )
{
$requiresModeration = FALSE;
$labels = NULL;
}
\IPS\Db::i()->delete( 'core_files_temp', array( 'contents=?', (string) $file ) );
$attachment = $file->makeAttachment( $postKey, \IPS\Member::loggedIn() );
$attachment = $file->makeAttachment( $postKey, \IPS\Member::loggedIn(), $requiresModeration, $labels );
return $attachment['attach_id'];
}
);
+9 -3
View File
@@ -130,6 +130,11 @@ class _Matrix extends \IPS\Helpers\Form
* @brief Form Id, Set it Matrix is part of a form
*/
public $formId = NULL;
/**
* @brief Determines whether the Row Titles are processed as raw html instead of a language string. Not needed if $langPrefix is set
*/
public $styledRowTitle = FALSE;
/**
* Get HTML
@@ -140,7 +145,7 @@ class _Matrix extends \IPS\Helpers\Form
{
try
{
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->matrix( $this->id, array_keys( $this->columns ), $this->elements(), $this->action, $this->hiddenValues, $this->actionButtons, $this->langPrefix, $this->calculateWidths(), $this->manageable, $this->checkAlls, $this->checkAllRows, $this->classes, $this->showTooltips, $this->squashFields, $this->sortable );
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->matrix( $this->id, array_keys( $this->columns ), $this->elements(), $this->action, $this->hiddenValues, $this->actionButtons, $this->langPrefix, $this->calculateWidths(), $this->manageable, $this->checkAlls, $this->checkAllRows, $this->classes, $this->showTooltips, $this->squashFields, $this->sortable, $this->styledRowTitle );
}
catch ( \Exception $e )
{
@@ -159,7 +164,7 @@ class _Matrix extends \IPS\Helpers\Form
*/
public function nested()
{
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->matrixNested( $this->id, array_keys( $this->columns ), $this->elements(), $this->action, $this->hiddenValues, $this->actionButtons, $this->langPrefix, $this->calculateWidths(), $this->manageable, $this->checkAlls, $this->checkAllRows, $this->classes, $this->showTooltips, $this->squashFields, $this->sortable );
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->matrixNested( $this->id, array_keys( $this->columns ), $this->elements(), $this->action, $this->hiddenValues, $this->actionButtons, $this->langPrefix, $this->calculateWidths(), $this->manageable, $this->checkAlls, $this->checkAllRows, $this->classes, $this->showTooltips, $this->squashFields, $this->sortable, $this->styledRowTitle );
}
/**
@@ -402,7 +407,8 @@ class _Matrix extends \IPS\Helpers\Form
if ( isset( $element->options['unlimited'] ) )
{
$unlimitedKey = "{$rowId}[{$columnName}_unlimited]";
if ( $value = \IPS\Request::i()->valueFromArray( $unlimitedKey ) )
$value = \IPS\Request::i()->valueFromArray( $unlimitedKey );
if ( $value !== NULL )
{
$element->value = $value;
}
+2 -1
View File
@@ -297,7 +297,8 @@ class _Node extends FormAbstract
{
foreach ( $this->value as $node )
{
$title = isset( $node::$titleLangPrefix ) ? \IPS\Member::loggedIn()->language()->addToStack( $node::$titleLangPrefix . $node->_id, FALSE, array( 'json' => TRUE, 'escape' => TRUE, 'striptags'=> TRUE ) ) : $node->_title;
$title = isset( $node::$titleLangPrefix ) ? \IPS\Member::loggedIn()->language()->addToStack( $node::$titleLangPrefix . $node->_id, FALSE, array( 'json' => TRUE, 'escape' => TRUE, 'striptags'=> TRUE ) ) : str_replace( "&#039;", "&apos;", htmlspecialchars( $node->_title, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', false ) );
$selected[ !( $node instanceof $nodeClass ) ? "{$node->_id}.s" : $node->_id ] = array( 'title' => $title, 'parents' => array_values( array_map( function( $val ){
if( isset( $val::$titleLangPrefix ) )
{
+13 -13
View File
@@ -184,15 +184,9 @@ class _Number extends FormAbstract
do the thousand seperator in case what we received was the former alredy, which would turn
for example "10,00" into "1000" */
$value = str_replace( trim( \IPS\Member::loggedIn()->language()->locale['decimal_point'] ), '.', $value );
/* If it's not numeric, throw an exception */
if ( ( !\is_numeric( $value ) and $value !== '' and $this->required === FALSE ) or ( !\is_numeric( $value ) and $this->required === TRUE ) )
{
throw new \InvalidArgumentException( 'form_number_bad' );
}
/* Convert to int/float */
if ( $this->options['decimals'] )
if ( $this->options['decimals'] and $value != '' )
{
$value = \floatval( $value );
if ( \is_int( $this->options['decimals'] ) )
@@ -202,7 +196,7 @@ class _Number extends FormAbstract
}
else
{
$value = \intval( $value );
$value = ( $value != '' ) ? \intval( $value ) : '';
}
/* Return */
@@ -216,16 +210,22 @@ class _Number extends FormAbstract
* @return TRUE
*/
public function validate()
{
{
parent::validate();
if ( $this->options['unlimited'] === NULL or $this->value !== $this->options['unlimited'] )
{
if ( $this->options['min'] !== NULL and $this->value < $this->options['min'] )
{
/* If it's not numeric, throw an exception */
if ( ( !\is_numeric( $this->value ) and $this->value !== '' and $this->required === FALSE ) or ( !\is_numeric( $this->value ) and $this->required === TRUE ) )
{
throw new \InvalidArgumentException( 'form_number_bad' );
}
if ( $this->options['min'] !== NULL and (int) $this->value < $this->options['min'] )
{
throw new \LengthException( \IPS\Member::loggedIn()->language()->addToStack('form_number_min', FALSE, array( 'sprintf' => array( $this->options['min'] ) ) ) );
}
if ( $this->options['max'] !== NULL and $this->value > $this->options['max'] )
if ( $this->options['max'] !== NULL and (int) $this->value > $this->options['max'] )
{
throw new \LengthException( \IPS\Member::loggedIn()->language()->addToStack('form_number_max', FALSE, array( 'sprintf' => array( $this->options['max'] ) ) ) );
}
-1
View File
@@ -59,7 +59,6 @@ class _Poll extends FormAbstract
foreach ( $choice['choice'] as $i => $question )
{
$thisChoice['title'] = $question;
$thisChoice['count'] = isset( $choice['votes'][ $i ] ) ? $choice['votes'][ $i ] : 0;
$thisQuestion['choices'][ $i ] = $thisChoice;
}
-4
View File
@@ -333,10 +333,6 @@ class _Tel extends Text
array (
0 => '33',
),
'FX' =>
array (
0 => '33',
),
'GA' =>
array (
0 => '241',
+6 -1
View File
@@ -311,6 +311,11 @@ class _Text extends TextArea
/* Set up profanity filters */
foreach( \IPS\core\Profanity::getProfanity() AS $profanity )
{
if ( $profanity->action == 'block' )
{
continue;
}
if ( $profanity->m_exact )
{
$exactProfanity[ $profanity->type ] = $profanity;
@@ -441,7 +446,7 @@ class _Text extends TextArea
}
else
{
if ( mb_stristr( $value, $type ) )
if ( mb_stristr( $this->value[ $k ], $type ) )
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'form_tags_not_allowed', FALSE, array( 'sprintf' => array( $v ) ) ) );
}
+9 -1
View File
@@ -78,7 +78,15 @@ class _Translatable extends FormAbstract
{
if ( \is_array( $this->options['sprintf'] ) )
{
$values[ $k ] = vsprintf( $v, $this->options['sprintf'] );
try
{
$values[ $k ] = vsprintf( $v, $this->options[ 'sprintf' ] );
}
catch ( \ValueError $e )
{
/* Note in pre-PHP8, vsprintf() returns FALSE */
$values[ $k ] = FALSE;
}
}
else
{
+61 -34
View File
@@ -45,6 +45,7 @@ class _Upload extends FormAbstract
'obscure' => TRUE, // Controls if an md5 hash should be added to the filename. *Must* be TRUE unless the uploaded files are public to all users (like emoticons),
'supportsDelete' => TRUE // Should the uploaded file be direct deletable from the upload field
'allowStockPhotos' => FALSE // If Pixabay enhancement is enabled, this will allow users to select stock art from Pixabay to upload
'canBeModerated' => TRUE // Set to TRUE if this uploader is capable of holding the submitted content for moderation
);
* @endcode
*/
@@ -67,7 +68,8 @@ class _Upload extends FormAbstract
'default' => NULL,
'obscure' => TRUE,
'supportsDelete' => TRUE,
'allowStockPhotos' => FALSE
'allowStockPhotos' => FALSE,
'canBeModerated' => FALSE
);
/**
@@ -167,7 +169,7 @@ class _Upload extends FormAbstract
/* If this has to be an image, set the allowed file types */
if ( isset( $options['image'] ) and !isset( $options['image']['optional'] ) and !isset( $options['allowedFileTypes'] ) )
{
$options['allowedFileTypes'] = \IPS\Image::supportedExtensions();
$options['allowedFileTypes'] = \IPS\Image::$imageExtensions;
}
/* Call parent constructor */
@@ -256,12 +258,16 @@ class _Upload extends FormAbstract
$image->setExifData( $fileObj->contents() );
}
$hasBeenResized = FALSE;
if( \is_array( $this->options['image'] ) )
{
$image->resizeToMax( $this->options['image']['maxWidth'] ?: NULL, $this->options['image']['maxHeight'] ?: NULL );
$hasBeenResized = $image->resizeToMax( $this->options['image']['maxWidth'] ?: NULL, $this->options['image']['maxHeight'] ?: NULL );
}
$fileObj->replace( (string) $image );
if ( $hasBeenResized or $image->hasBeenRotated )
{
$fileObj->replace( (string) $image );
}
}
catch ( \Exception $e ) { }
}
@@ -441,7 +447,6 @@ class _Upload extends FormAbstract
'size' => \IPS\Output\Plugin\Filesize::humanReadableFilesize( $file->filesize(), FALSE, TRUE ),
'sizeRaw' => $file->filesize(),
'default' => ( isset( $attachment ) AND $attachment['default'] ) ? $attachment['default'] : NULL,
'securityKey' => ( isset( $attachment ) ) ? $attachment['attach_security_key'] : $file->securityKey
);
}
catch( \UnderflowException $e ){}
@@ -482,7 +487,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', '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', 'core_files_temp', array( 'upload_key=?', md5( $this->name . session_id() ) ) )->setKeyField('id') );
if ( $this->options['storageExtension'] )
{
$existingName = $this->name . '_existing';
@@ -501,6 +506,7 @@ class _Upload extends FormAbstract
/* Reset the original filename to the real file name as $this->originalFilename is generated from the now AWS-safe $this->filename */
$file->originalFilename = $tempFiles[ $tempId ]['filename'];
$file->exifData = $tempFiles[ $tempId ]['exif'] ? json_decode( $tempFiles[ $tempId ]['exif'], TRUE ) : NULL;
$file->requiresModeration = (bool) $tempFiles[ $tempId ]['requires_moderation'];
$return[ $id ] = $file;
}
else
@@ -701,7 +707,7 @@ class _Upload extends FormAbstract
$ext = mb_strtolower( mb_substr( $file['name'], ( mb_strrpos( $file['name'], '.' ) + 1 ) ) );
/* Don't allow "XSS" in images */
if( \in_array( $ext, \IPS\File::$safeFileExtensions ) AND \in_array( $ext, \IPS\Image::supportedExtensions() ) )
if( \in_array( $ext, \IPS\File::$safeFileExtensions ) AND \in_array( $ext, \IPS\Image::$imageExtensions ) )
{
if( \IPS\File::checkXssInFile( file_get_contents( $file['tmp_name'] ) ) )
{
@@ -718,36 +724,42 @@ class _Upload extends FormAbstract
else
{
$options = $this->options;
$fileObjects = \IPS\File::createFromUploads( $this->options['storageExtension'], $fieldName, $this->options['allowedFileTypes'], $this->options['maxFileSize'], $this->options['totalMaxSize'], 0, function( $contents, $filename ) use ( $options )
$canBeModerated = $options['canBeModerated'];
$fileObjects = \IPS\File::createFromUploads( $this->options['storageExtension'], $fieldName, $this->options['allowedFileTypes'], $this->options['maxFileSize'], $this->options['totalMaxSize'], 0, function( $contents, $filename, $i ) use ( $options, &$requiresModeration, $canBeModerated )
{
$ext = mb_strtolower( mb_substr( $filename, mb_strrpos( $filename, '.' ) + 1 ) );
/* Resize images */
if ( \in_array( $ext, \IPS\Image::supportedExtensions() ) AND ( \is_array( $options['image'] ) OR $options['checkImage'] ) )
/* Do image-specific stuff */
if ( \in_array( $ext, \IPS\Image::$imageExtensions ) )
{
try
/* Resize */
$image = NULL;
$hasBeenResized = FALSE;
if ( \is_array( $options['image'] ) OR $options['checkImage'] )
{
$image = \IPS\Image::create( $contents );
if( $image::exifSupported() )
try
{
$image->setExifData( $contents );
$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;
}
}
if( \is_array( $options['image'] ) )
{
$image->resizeToMax( $options['image']['maxWidth'] ?: NULL, $options['image']['maxHeight'] ?: NULL );
}
return (string) $image;
catch ( \Exception $e ) {}
}
catch ( \Exception $e ) {}
}
return $contents;
}, $this->options['storageContainer'], $this->options['obscure'] );
foreach ( $fileObjects as $fileObj )
foreach ( $fileObjects as $i => $fileObj )
{
$exif = NULL;
@@ -756,15 +768,17 @@ class _Upload extends FormAbstract
$exif = json_encode( $fileObj->exifData );
}
$insertId = \IPS\Db::i()->insert( 'core_files_temp', array(
'upload_key' => md5( $this->name . session_id() ),
'filename' => $fileObj->originalFilename,
'mime' => \IPS\File::getMimeType( $fileObj->originalFilename ),
'contents' => (string) $fileObj,
'time' => time(),
'storage_extension' => $this->options['storageExtension'],
'exif' => $exif
) );
$data = $this->populateTempData( array(
'upload_key' => md5( $this->name . session_id() ),
'filename' => $fileObj->originalFilename,
'mime' => \IPS\File::getMimeType( $fileObj->originalFilename ),
'contents' => (string) $fileObj,
'time' => time(),
'storage_extension' => $this->options['storageExtension'],
'exif' => $exif
), $fileObj, $options );
$insertId = \IPS\Db::i()->insert( 'core_files_temp', $data );
if ( $this->options['callback'] )
{
@@ -799,6 +813,19 @@ class _Upload extends FormAbstract
throw new \InvalidArgumentException('form_required');
}
}
/**
* Finish populating the data before its inserted
*
* @param array $data Array of data to change/add to
* @param \IPS\File $fileObj File Object data
* @param array $options Field options
* @return array
*/
protected function populateTempData( array $data, \IPS\File $fileObj, array $options ): array
{
return $data;
}
/**
* String Value
+50 -9
View File
@@ -121,6 +121,11 @@ class _Content extends Table
* @brief Get reactions for content item if supported
*/
protected $getReactions = FALSE;
/**
* @brief If passing a container, limit content by that container
*/
protected $limitByContainer = TRUE;
/**
* Constructor
@@ -139,7 +144,7 @@ class _Content extends Table
* @param bool $getReactions Get reactions for this item
* @return void
*/
public function __construct( $class, \IPS\Http\Url $baseUrl, $where=NULL, \IPS\Node\Model $container=NULL, $includeHidden=\IPS\Content\Hideable::FILTER_AUTOMATIC, $permCheck='view', $honorPinned=TRUE, $showMovedLinks=FALSE, $callback=NULL, $getFirstComment=FALSE, $getFollowerCount=FALSE, $getReactions=FALSE )
public function __construct( $class, \IPS\Http\Url $baseUrl, $where=NULL, \IPS\Node\Model $container=NULL, $includeHidden=\IPS\Content\Hideable::FILTER_AUTOMATIC, $permCheck='view', $honorPinned=TRUE, $showMovedLinks=FALSE, $callback=NULL, $getFirstComment=FALSE, $getFollowerCount=FALSE, $getReactions=FALSE, $limitByContainer=TRUE )
{
/* Init */
$this->include = array();
@@ -154,7 +159,8 @@ class _Content extends Table
$this->getFirstComment = $getFirstComment;
$this->getFollowerCount = $getFollowerCount;
$this->getReactions = $getReactions;
$this->limitByContainer = $limitByContainer;
/* Init */
parent::__construct( $baseUrl );
@@ -163,7 +169,11 @@ class _Content extends Table
/* Set container */
if ( $container !== NULL )
{
$this->where[] = array( $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['container'] . '=?', $container->_id );
if ( $this->limitByContainer )
{
$this->where[] = array($class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['container'] . '=?', $container->_id);
}
$this->sortDirection = ( $this->sortDirection !== NULL ) ? $this->sortDirection : $container->_sortOrder;
if ( !$this->sortBy and ! empty( $container->_sortBy ) )
@@ -210,6 +220,7 @@ class _Content extends Table
\IPS\Output::i()->metaTags['og:title'] = $container->metaTitle();
\IPS\Output::i()->metaTags['og:description'] = $container->metaDescription();
\IPS\Output::i()->linkTags['canonical'] = (string) $canonicalUrl;
\IPS\Output::i()->metaTags['og:url'] = (string) $canonicalUrl;
}
/* Set available sort options */
@@ -280,7 +291,7 @@ class _Content extends Table
}
/* If we are limiting to a single container, check to see if this user can view them */
if ( $this->container and $this->permCheck )
if ( ( $this->container and $this->limitByContainer ) and $this->permCheck )
{
try
{
@@ -338,8 +349,8 @@ class _Content extends Table
}
/* Get results */
$this->count = $class::getItemsWithPermission( $where, $sortBy, NULL, $this->permCheck, $this->includeHiddenContent, 0, NULL, $this->joinContainer, $this->joinComments, $this->joinReviews, TRUE, $this->joins, ( $this->container ) ? $this->container : FALSE, TRUE, TRUE, TRUE, $this->showMovedLinks );
$it = $class::getItemsWithPermission( $where, $sortBy, array( ( $this->limit * ( $this->page - 1 ) ), $this->limit ), $this->permCheck, $this->includeHiddenContent, 0, NULL, $this->joinContainer, $this->joinComments, $this->joinReviews, FALSE, $this->joins, ( $this->container ) ? $this->container : FALSE, TRUE, TRUE, TRUE, $this->showMovedLinks );
$this->count = $class::getItemsWithPermission( $where, $sortBy, NULL, $this->permCheck, $this->includeHiddenContent, 0, NULL, $this->joinContainer, $this->joinComments, $this->joinReviews, TRUE, $this->joins, ( $this->container and $this->limitByContainer ) ? $this->container : FALSE, TRUE, TRUE, TRUE, $this->showMovedLinks );
$it = $class::getItemsWithPermission( $where, $sortBy, array( ( $this->limit * ( $this->page - 1 ) ), $this->limit ), $this->permCheck, $this->includeHiddenContent, 0, NULL, $this->joinContainer, $this->joinComments, $this->joinReviews, FALSE, $this->joins, ( $this->container and $this->limitByContainer ) ? $this->container : FALSE, TRUE, TRUE, TRUE, $this->showMovedLinks );
$this->pages = ceil( $this->count / $this->limit );
$rows = iterator_to_array( $it );
$idField = $class::$databaseColumnId;
@@ -347,19 +358,49 @@ class _Content extends Table
if ( isset( $this->getFirstComment ) and isset( $class::$databaseColumnMap['first_comment_id'] ) and $class::$firstCommentRequired )
{
$commentIds = array();
$archivedCommentIds = [];
$firstCommentField = $class::$databaseColumnMap['first_comment_id'];
foreach( $rows as $item )
{
$commentIds[ $item->$idField ] = $item->$firstCommentField;
/* Get us the archived comment ids*/
if ( isset( $class::$archiveClass ) and method_exists( $class, 'isArchived' ) AND $item->isArchived() )
{
$archivedCommentIds[ $item->$idField ] = $item->$firstCommentField;
}
else
{
$commentIds[ $item->$idField ] = $item->$firstCommentField;
}
}
$comments = array();
if ( \count( $archivedCommentIds ) )
{
$archivedCommentClass = $class::$archiveClass;
$commentField = $archivedCommentClass::$databaseColumnId;
foreach( $archivedCommentClass::getItemsWithPermission( array( \IPS\Db::i()->in( $archivedCommentClass::$databasePrefix . $commentField, array_values( $archivedCommentIds ) ) ), NULL, $this->limit ) as $cObj )
{
$comments[ $cObj->$commentField ] = $cObj;
}
foreach( $rows as $id => $item )
{
if ( isset( $archivedCommentIds[ $item->$idField ] ) and isset( $comments[ $archivedCommentIds[ $item->$idField ] ] ) )
{
$rows[ $id ]->firstComment = $comments[ $archivedCommentIds[ $item->$idField ] ];
}
}
}
if ( \count( $commentIds ) )
{
$commentClass = $class::$commentClass;
$commentField = $commentClass::$databaseColumnId;
$comments = array();
foreach( $commentClass::getItemsWithPermission( array( \IPS\Db::i()->in( $commentClass::$databasePrefix . $commentField, array_values( $commentIds ) ) ), NULL, $this->limit ) as $cObj )
{
+3 -3
View File
@@ -48,7 +48,7 @@ class _Custom extends Table
* @param array $dataSource Data source
* @param \IPS\Http\Url $baseUrl Base URL
*/
public function __construct( $dataSource, \IPS\Http\Url $baseUrl )
public function __construct( $dataSource, \IPS\Http\Url $baseUrl, $count = NULL )
{
$this->dataSource = $dataSource;
$this->count = \count( $this->dataSource );
@@ -107,7 +107,7 @@ class _Custom extends Table
}
/* Do we need to sort? */
if( !empty( $rows ) and $this->sortBy and isset( $rows[ key( $rows ) ][ $this->sortBy ] ) )
if( $this->parseSource and !empty( $rows ) and $this->sortBy and isset( $rows[ key( $rows ) ][ $this->sortBy ] ) )
{
$sortBy = $this->sortBy;
$sortDirection = $this->sortDirection;
@@ -125,7 +125,7 @@ class _Custom extends Table
}
/* Limit */
if ( $this->limit and $this->count > $this->limit )
if ( $this->limit and $this->count > $this->limit and $this->parseSource )
{
$rows = \array_slice( $rows, ( $this->limit * ( $this->page - 1 ) ), $this->limit, TRUE );
}
+10 -3
View File
@@ -62,6 +62,11 @@ class _Db extends Table
*/
public $keyField = NULL;
/**
* @brief Group by key
*/
public $groupBy = NULL;
/**
* @brief The database we will query against
*/
@@ -320,9 +325,10 @@ class _Db extends Table
}
}
$count = $count->first();
$selectPrefix = ( $this->groupBy ) ? '' : $this->table . '.*, ';
/* Now get column headers */
$query = $this->db->select( ( \count( $selects ) ) ? $this->table . '.*, ' . implode( ', ', $selects ) : '*', $this->table, NULL, NULL, array( 0, 1 ) );
$query = $this->db->select( ( \count( $selects ) ) ? $selectPrefix . implode( ', ', $selects ) : '*', $this->table, NULL, NULL, array( 0, 1 ), $this->groupBy );
if ( \count( $this->joins ) )
{
@@ -380,11 +386,12 @@ class _Db extends Table
/* Run query */
$rows = array();
$select = $this->db->select(
( \count( $selects ) ) ? $this->table . '.*, ' . implode( ', ', $selects ) : '*',
( \count( $selects ) ) ? $selectPrefix . implode( ', ', $selects ) : '*',
$this->table,
$where,
$orderBy,
array( ( $this->limit * ( $this->page - 1 ) ), $limit )
array( ( $this->limit * ( $this->page - 1 ) ), $limit ),
$this->groupBy
);
if ( $this->index )
+7 -1
View File
@@ -138,6 +138,11 @@ abstract class _Table
* @brief Language key for table title
*/
public $title = '';
/**
* @brief Use realtime features within the table?
*/
public $enableRealtime = false;
/**
* @brief Parsers
@@ -609,7 +614,7 @@ abstract class _Table
$return[ $k ] = $v->_id;
}
}
else if( isset( $this->advancedSearch[ mb_substr( $k, mb_strlen( $this->langPrefix ) ) ] ) and $this->advancedSearch[ mb_substr( $k, mb_strlen( $this->langPrefix ) ) ] === \IPS\Helpers\Table\SEARCH_BOOL )
else if( isset( $this->advancedSearch[ mb_substr( $k, mb_strlen( $this->langPrefix ) ) ][0] ) and $this->advancedSearch[ mb_substr( $k, mb_strlen( $this->langPrefix ) ) ][0] === \IPS\Helpers\Table\SEARCH_BOOL )
{
$return[ $k . '_checkbox' ] = $v;
}
@@ -618,6 +623,7 @@ abstract class _Table
$return[ $k ] = $v;
}
}
return $return;
}
+8 -1
View File
@@ -63,6 +63,11 @@ class _Curl
* @brief Data sent
*/
protected $dataForLog = NULL;
/**
* @brief Headers sent
*/
protected $headersForLog = [];
/**
* Contructor
@@ -212,6 +217,8 @@ class _Curl
$extra[] = "{$k}: {$v}";
break;
}
$this->headersForLog[ $k ] = "{$k}: {$v}";
}
if ( !empty( $extra ) )
@@ -382,7 +389,7 @@ class _Curl
/* Log - but because the output can be large, only do this if we explicitly have debug logging enabled */
if ( \defined('\IPS\DEBUG_LOG') and \IPS\DEBUG_LOG )
{
\IPS\Log::debug( "\n\n------------------------------------\ncURL REQUEST: {$this->url}\n------------------------------------\n\n" . var_export( $this->dataForLog, TRUE ) . "\n\n------------------------------------\nRESPONSE\n------------------------------------\n\n" . $output, 'request' );
\IPS\Log::debug( "\n\n------------------------------------\ncURL REQUEST: {$this->url}\n------------------------------------\n\n" . implode( "\n", $this->headersForLog ) . "\n\n" . var_export( $this->dataForLog, TRUE ) . "\n\n------------------------------------\nRESPONSE\n------------------------------------\n\n" . $output, 'request' );
}
/* Errors? */
+3 -1
View File
@@ -232,9 +232,11 @@ class _Sockets
$request = mb_strtoupper( $method ) . ' /' . ltrim( $location, '/' ) . " HTTP/{$this->httpVersion}\r\n";
$request .= "Host: {$this->url->data['host']}" . ( isset( $this->url->data['port'] ) ? ":{$this->url->data['port']}" : '' ) . "\r\n";
$headersForLog = [];
foreach ( $this->headers as $k => $v )
{
$request .= "{$k}: {$v}\r\n";
$headersForLog[ $k ] = "{$k}: {$v}";
}
$request .= "Connection: Close\r\n";
@@ -264,7 +266,7 @@ class _Sockets
/* Log - but because the output can be large, only do this if we explicitly have debug logging enabled */
if ( \defined('\IPS\DEBUG_LOG') and \IPS\DEBUG_LOG )
{
\IPS\Log::debug( "\n\n------------------------------------\nSOCKETS REQUEST: {$this->url}\n------------------------------------\n\n{$request}\n\n------------------------------------\nRESPONSE\n------------------------------------\n\n" . $response, 'request' );
\IPS\Log::debug( "\n\n------------------------------------\nSOCKETS REQUEST: {$this->url}\n------------------------------------\n\n" . implode( "\n", $headersForLog ) . "\n\n{$request}\n\n------------------------------------\nRESPONSE\n------------------------------------\n\n" . $response, 'request' );
}
/* Interpret response */
+11
View File
@@ -278,4 +278,15 @@ class _Response
return $queryString;
}
/**
* Is this response successful ? Was the statuscode between 200 and 300
*
* @return bool
*/
public function isSuccessful(): bool
{
/* Return if we have received a 2xx response code */
return $this->httpResponseCode >= 200 && $this->httpResponseCode < 300;
}
}
+8
View File
@@ -48,6 +48,11 @@ class _Url
*/
const PROTOCOL_RELATIVE = 3;
/**
* @brief Use no protocol at all!
*/
const PROTOCOL_WITHOUT = 4;
/**
* @brief Scheme component
*/
@@ -781,6 +786,9 @@ class _Url
case static::PROTOCOL_RELATIVE:
$url = '//' . mb_substr( $url, mb_strpos( $url, '://' ) + 3 );
break;
case static::PROTOCOL_WITHOUT:
$url = mb_substr( $url, mb_strpos( $url, '://' ) + 3 );
break;
}
}
-6
View File
@@ -26,11 +26,6 @@ class _Useragent
* @brief Search engine spider?
*/
public $spider = FALSE;
/**
* @brief Invision Community mobile app?
*/
public $ipsApp = FALSE;
/**
* @brief Browser name
@@ -110,7 +105,6 @@ class _Useragent
$this->browser = 'InvisionCommunityApp';
$this->browserVersion = $matches[1];
$this->ipsApp = $matches[2];
return;
}
+1 -1
View File
@@ -407,7 +407,7 @@ class _Gd extends \IPS\Image
{
$extensions = static::$imageExtensions;
if( isset( gd_info()['WebP Support'] ) and gd_info()['WebP Support'] )
if( isset( gd_info()['WebP Support'] ) and gd_info()['WebP Support'] and version_compare( PHP_VERSION, '7.3.0' ) >= 0 )
{
$extensions[] = 'webp';
}
+10 -3
View File
@@ -765,7 +765,7 @@ class _Lang extends \IPS\Node\Model
* Add to output stack
*
* @param string $key Language key
* @param bool $vle Add VLE tags?
* @param bool|null $vle Add VLE tags?
* @param array $options Options
* @return string Unique id
*/
@@ -1455,6 +1455,7 @@ class _Lang extends \IPS\Node\Model
'icon' => 'download',
'title' => 'download',
'link' => \IPS\Http\Url::internal( "app=core&module=languages&controller=languages&do=download&id={$this->_id}" ),
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => $this->_title )
);
}
@@ -1727,7 +1728,7 @@ class _Lang extends \IPS\Node\Model
if ( mb_stristr( $output, '{dir}' ) )
{
$output = preg_replace( '#(<td\s+?)dir=([\'"]){dir}([\'"])#i', '\1dir=\2' . $dir . '\3', $output );
$output = preg_replace( '#(<td.+?)dir=([\'"]){dir}([\'"])#i', '\1dir=\2' . $dir . '\3', $output );
preg_match_all( '#<(body|html)([^>]+?)>#i', $output, $matches, PREG_SET_ORDER );
@@ -1954,7 +1955,7 @@ class _Lang extends \IPS\Node\Model
throw new \ErrorException;
}
}
catch ( \ErrorException $e )
catch ( \Throwable $e )
{
// If there's the wrong number of parameters because the translator's done it wrong, we can just use empty strings for replacements
}
@@ -2050,6 +2051,12 @@ class _Lang extends \IPS\Node\Model
$replacement = \IPS\Text\Parser::removeLazyLoad( $replacement );
}
/* Remove newlines and any whitespace */
if ( isset( $values[ 'options' ][ 'removeNewlines' ] ) )
{
$replacement = trim( preg_replace( "#\n\s+?#", " ", $replacement ) );
}
$replacements[ $key ] = $replacement;
}
+48 -1
View File
@@ -106,7 +106,11 @@ class _Log extends \IPS\Patterns\ActiveRecord
$log->url = \IPS\Request::i()->url();
$log->member_id = \IPS\Member::loggedIn()->member_id ?: 0;
}
if ( \IPS\Application::load('core')->long_version >= 106149 ) #Column was added in 106149
{
$log->loaded_hooks = json_encode( \IPS\IPS::$loadedHooks );
}
$log->category = $category;
$log->save();
@@ -235,4 +239,47 @@ class _Log extends \IPS\Patterns\ActiveRecord
}
}
}
/**
* Get Loaded Hooks
*
* @return array
*/
public function get_loaded_hooks()
{
return ( $this->_data['loaded_hooks'] ) ? json_decode( $this->_data['loaded_hooks'] ) : array();
}
/**
* Get Hook details
*
* @param string $file filename of loaded hook
* @return array
*/
public static function hookDetails( $file )
{
$bits = explode( '/', $file );
$return = array();
/* Plugins are always third party */
if ( $bits[0] === 'plugins' )
{
$plugin = NULL;
try
{
$plugin = \IPS\Plugin::getPluginFromPath( "/" . $file );
}
catch ( \Exception $e ) {}
$return['type'] = 'plugin';
$return['id'] = ( $plugin ) ? $plugin->id : NULL;
}
else
{
$return['type'] = 'app';
$return['id'] = $bits[1];
}
return $return;
}
}
Loaded 100 of 1933 files, more files were not shown because too many files have changed in this diff. Show more