Version 4.7.0
This commit is contained in:
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
@@ -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
@@ -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
@@ -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
|
||||
+11
@@ -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
|
||||
@@ -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;
|
||||
|
||||
+4
-1
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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 )
|
||||
|
||||
@@ -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
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -83,7 +83,7 @@ class _PaginatedResponse extends Response
|
||||
/**
|
||||
* Data to output
|
||||
*
|
||||
* @return string
|
||||
* @return array
|
||||
*/
|
||||
public function getOutput()
|
||||
{
|
||||
|
||||
+171
-24
@@ -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 );
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 ) );
|
||||
}
|
||||
}
|
||||
@@ -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'] ) )
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 ) )
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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 )
|
||||
{
|
||||
|
||||
@@ -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 ] ] );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ) )
|
||||
{
|
||||
|
||||
@@ -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" );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 );
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ){}
|
||||
|
||||
@@ -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,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'];
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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' ) )
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 );
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 )
|
||||
{
|
||||
|
||||
@@ -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' ) )
|
||||
|
||||
@@ -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
@@ -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
@@ -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++;
|
||||
}
|
||||
|
||||
@@ -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'] )
|
||||
|
||||
@@ -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 ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 );
|
||||
}
|
||||
|
||||
@@ -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 )
|
||||
{
|
||||
|
||||
@@ -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 );
|
||||
|
||||
@@ -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
@@ -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
@@ -78,7 +78,10 @@ class _Ftp
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
@ftp_close( $this->ftp );
|
||||
if( $this->ftp !== NULL )
|
||||
{
|
||||
@ftp_close( $this->ftp );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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?
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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'] );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
@@ -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'];
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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( "'", "'", 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 ) )
|
||||
{
|
||||
|
||||
@@ -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'] ) ) ) );
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -333,10 +333,6 @@ class _Tel extends Text
|
||||
array (
|
||||
0 => '33',
|
||||
),
|
||||
'FX' =>
|
||||
array (
|
||||
0 => '33',
|
||||
),
|
||||
'GA' =>
|
||||
array (
|
||||
0 => '241',
|
||||
|
||||
@@ -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 ) ) ) );
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 )
|
||||
{
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
|
||||
@@ -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 )
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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? */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
Reference in new issue
Block a user