Version 4.7.13
This commit is contained in:
1 parent
fd22d99e69
commit
7124a02564
954 files changed
+29277
-59308
No files matched your search
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
// DO NOT EDIT THIS FILE - IT IS GENERATED BY constant_generator.php
|
||||
|
||||
namespace donatj\UserAgent;
|
||||
|
||||
interface Browsers {
|
||||
|
||||
const ADSBOT_GOOGLE = 'AdsBot-Google';
|
||||
const ANDROID_BROWSER = 'Android Browser';
|
||||
const APPLEBOT = 'Applebot';
|
||||
const BAIDUSPIDER = 'Baiduspider';
|
||||
const BINGBOT = 'bingbot';
|
||||
const BLACKBERRY_BROWSER = 'BlackBerry Browser';
|
||||
const BROWSER = 'Browser';
|
||||
const BUNJALLOO = 'Bunjalloo';
|
||||
const CAMINO = 'Camino';
|
||||
const CHROME = 'Chrome';
|
||||
const CURL = 'curl';
|
||||
const EDGE = 'Edge';
|
||||
const FACEBOOKEXTERNALHIT = 'facebookexternalhit';
|
||||
const FEEDVALIDATOR = 'FeedValidator';
|
||||
const FIREFOX = 'Firefox';
|
||||
const GOOGLEBOT = 'Googlebot';
|
||||
const GOOGLEBOT_IMAGE = 'Googlebot-Image';
|
||||
const GOOGLEBOT_VIDEO = 'Googlebot-Video';
|
||||
const HEADLESSCHROME = 'HeadlessChrome';
|
||||
const IEMOBILE = 'IEMobile';
|
||||
const IMESSAGEBOT = 'iMessageBot';
|
||||
const KINDLE = 'Kindle';
|
||||
const LYNX = 'Lynx';
|
||||
const MIDORI = 'Midori';
|
||||
const MIUIBROWSER = 'MiuiBrowser';
|
||||
const MSIE = 'MSIE';
|
||||
const MSNBOT_MEDIA = 'msnbot-media';
|
||||
const NETFRONT = 'NetFront';
|
||||
const NINTENDOBROWSER = 'NintendoBrowser';
|
||||
const OCULUSBROWSER = 'OculusBrowser';
|
||||
const OPERA = 'Opera';
|
||||
const PUFFIN = 'Puffin';
|
||||
const SAFARI = 'Safari';
|
||||
const SAILFISHBROWSER = 'SailfishBrowser';
|
||||
const SAMSUNGBROWSER = 'SamsungBrowser';
|
||||
const SILK = 'Silk';
|
||||
const TELEGRAMBOT = 'TelegramBot';
|
||||
const TIZENBROWSER = 'TizenBrowser';
|
||||
const TWITTERBOT = 'Twitterbot';
|
||||
const UC_BROWSER = 'UC Browser';
|
||||
const VALVE_STEAM_TENFOOT = 'Valve Steam Tenfoot';
|
||||
const VIVALDI = 'Vivaldi';
|
||||
const WGET = 'Wget';
|
||||
const WORDPRESS = 'WordPress';
|
||||
const YANDEX = 'Yandex';
|
||||
const YANDEXBOT = 'YandexBot';
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
// DO NOT EDIT THIS FILE - IT IS GENERATED BY constant_generator.php
|
||||
|
||||
namespace donatj\UserAgent;
|
||||
|
||||
interface Platforms {
|
||||
|
||||
const MACINTOSH = 'Macintosh';
|
||||
const CHROME_OS = 'Chrome OS';
|
||||
const LINUX = 'Linux';
|
||||
const WINDOWS = 'Windows';
|
||||
const ANDROID = 'Android';
|
||||
const BLACKBERRY = 'BlackBerry';
|
||||
const FREEBSD = 'FreeBSD';
|
||||
const IPAD = 'iPad';
|
||||
const IPHONE = 'iPhone';
|
||||
const IPOD = 'iPod';
|
||||
const KINDLE = 'Kindle';
|
||||
const KINDLE_FIRE = 'Kindle Fire';
|
||||
const NETBSD = 'NetBSD';
|
||||
const NEW_NINTENDO_3DS = 'New Nintendo 3DS';
|
||||
const NINTENDO_3DS = 'Nintendo 3DS';
|
||||
const NINTENDO_DS = 'Nintendo DS';
|
||||
const NINTENDO_SWITCH = 'Nintendo Switch';
|
||||
const NINTENDO_WII = 'Nintendo Wii';
|
||||
const NINTENDO_WIIU = 'Nintendo WiiU';
|
||||
const OPENBSD = 'OpenBSD';
|
||||
const PLAYBOOK = 'PlayBook';
|
||||
const PLAYSTATION_3 = 'PlayStation 3';
|
||||
const PLAYSTATION_4 = 'PlayStation 4';
|
||||
const PLAYSTATION_5 = 'PlayStation 5';
|
||||
const PLAYSTATION_VITA = 'PlayStation Vita';
|
||||
const SAILFISH = 'Sailfish';
|
||||
const SYMBIAN = 'Symbian';
|
||||
const TIZEN = 'Tizen';
|
||||
const WINDOWS_PHONE = 'Windows Phone';
|
||||
const XBOX = 'Xbox';
|
||||
const XBOX_ONE = 'Xbox One';
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace donatj\UserAgent;
|
||||
|
||||
class UserAgent implements UserAgentInterface {
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $platform;
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $browser;
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $browserVersion;
|
||||
|
||||
/**
|
||||
* UserAgent constructor.
|
||||
*
|
||||
* @param string|null $platform
|
||||
* @param string|null $browser
|
||||
* @param string|null $browserVersion
|
||||
*/
|
||||
public function __construct( $platform, $browser, $browserVersion ) {
|
||||
$this->platform = $platform;
|
||||
$this->browser = $browser;
|
||||
$this->browserVersion = $browserVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
* @see \donatj\UserAgent\Platforms for a list of tested platforms
|
||||
*/
|
||||
public function platform() {
|
||||
return $this->platform;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
* @see \donatj\UserAgent\Browsers for a list of tested browsers.
|
||||
*/
|
||||
public function browser() {
|
||||
return $this->browser;
|
||||
}
|
||||
|
||||
/**
|
||||
* The version string. Formatting depends on the browser.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function browserVersion() {
|
||||
return $this->browserVersion;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace donatj\UserAgent;
|
||||
|
||||
interface UserAgentInterface {
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
* @see \donatj\UserAgent\Platforms for a list of tested platforms
|
||||
*/
|
||||
public function platform();
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
* @see \donatj\UserAgent\Browsers for a list of tested browsers.
|
||||
*/
|
||||
public function browser();
|
||||
|
||||
/**
|
||||
* The version string. Formatting depends on the browser.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function browserVersion();
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace donatj\UserAgent;
|
||||
|
||||
class UserAgentParser {
|
||||
|
||||
/**
|
||||
* Parses a user agent string into its important parts, provide an object
|
||||
*
|
||||
* @param string|null $u_agent User agent string to parse or null. Uses $_SERVER['HTTP_USER_AGENT'] on NULL
|
||||
* @return UserAgent an object with 'browser', 'browserVersion' and 'platform' methods
|
||||
* @throws \InvalidArgumentException on not having a proper user agent to parse.
|
||||
* @see \donatj\UserAgent\parse_user_agent()
|
||||
*
|
||||
*/
|
||||
public function parse( $u_agent = null ) {
|
||||
$parsed = parse_user_agent($u_agent);
|
||||
|
||||
return new UserAgent(
|
||||
$parsed[PLATFORM],
|
||||
$parsed[BROWSER],
|
||||
$parsed[BROWSER_VERSION]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a user agent string into its important parts
|
||||
*
|
||||
* @param string|null $u_agent User agent string to parse or null. Uses $_SERVER['HTTP_USER_AGENT'] on NULL
|
||||
* @return UserAgent an object with 'browser', 'browserVersion' and 'platform' methods
|
||||
* @throws \InvalidArgumentException on not having a proper user agent to parse.
|
||||
* @see \donatj\UserAgent\parse_user_agent()
|
||||
*
|
||||
*/
|
||||
public function __invoke( $u_agent = null ) {
|
||||
return $this->parse($u_agent);
|
||||
}
|
||||
|
||||
}
|
||||
+181
-144
@@ -1,181 +1,218 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Parses a user agent string into its important parts
|
||||
*
|
||||
* @param string|null $u_agent User agent string to parse or null. Uses $_SERVER['HTTP_USER_AGENT'] on NULL
|
||||
* @return string[] an array with browser, version and platform keys
|
||||
* @throws \InvalidArgumentException on not having a proper user agent to parse.
|
||||
*
|
||||
* @author Jesse G. Donat <donatj@gmail.com>
|
||||
*
|
||||
* @link https://donatstudios.com/PHP-Parser-HTTP_USER_AGENT
|
||||
* @link https://github.com/donatj/PhpUserAgent
|
||||
*
|
||||
* @license MIT
|
||||
* @license MIT https://github.com/donatj/PhpUserAgent/blob/master/LICENSE.md
|
||||
*/
|
||||
function parse_user_agent( $u_agent = null ) {
|
||||
if( $u_agent === null && isset($_SERVER['HTTP_USER_AGENT']) ) {
|
||||
$u_agent = $_SERVER['HTTP_USER_AGENT'];
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* Parses a user agent string into its important parts
|
||||
*
|
||||
* This method is defined for backwards comparability with the old global method.
|
||||
*
|
||||
* @param string|null $u_agent User agent string to parse or null. Uses $_SERVER['HTTP_USER_AGENT'] on NULL
|
||||
* @return string[] an array with 'browser', 'version' and 'platform' keys
|
||||
* @throws \InvalidArgumentException on not having a proper user agent to parse.
|
||||
*
|
||||
* @deprecated This exists for backwards compatibility with 0.x and will likely be removed in 2.x
|
||||
* @see \donatj\UserAgent\parse_user_agent
|
||||
*/
|
||||
function parse_user_agent( $u_agent = null ) {
|
||||
return \donatj\UserAgent\parse_user_agent($u_agent);
|
||||
}
|
||||
}
|
||||
|
||||
if( $u_agent === null ) {
|
||||
throw new \InvalidArgumentException('parse_user_agent requires a user agent');
|
||||
}
|
||||
namespace donatj\UserAgent {
|
||||
|
||||
$platform = null;
|
||||
$browser = null;
|
||||
$version = null;
|
||||
const PLATFORM = 'platform';
|
||||
const BROWSER = 'browser';
|
||||
const BROWSER_VERSION = 'version';
|
||||
|
||||
$empty = array( 'platform' => $platform, 'browser' => $browser, 'version' => $version );
|
||||
/**
|
||||
* Parses a user agent string into its important parts
|
||||
*
|
||||
* @param string|null $u_agent User agent string to parse or null. Uses $_SERVER['HTTP_USER_AGENT'] on NULL
|
||||
* @return string[] an array with 'browser', 'version' and 'platform' keys
|
||||
* @throws \InvalidArgumentException on not having a proper user agent to parse.
|
||||
*/
|
||||
function parse_user_agent( $u_agent = null ) {
|
||||
if( $u_agent === null && isset($_SERVER['HTTP_USER_AGENT']) ) {
|
||||
$u_agent = (string)$_SERVER['HTTP_USER_AGENT'];
|
||||
}
|
||||
|
||||
if( !$u_agent ) {
|
||||
return $empty;
|
||||
}
|
||||
if( $u_agent === null ) {
|
||||
throw new \InvalidArgumentException('parse_user_agent requires a user agent');
|
||||
}
|
||||
|
||||
if( preg_match('/\((.*?)\)/m', $u_agent, $parent_matches) ) {
|
||||
preg_match_all('/(?P<platform>BB\d+;|Android|CrOS|Tizen|iPhone|iPad|iPod|Linux|(Open|Net|Free)BSD|Macintosh|Windows(\ Phone)?|Silk|linux-gnu|BlackBerry|PlayBook|X11|(New\ )?Nintendo\ (WiiU?|3?DS|Switch)|Xbox(\ One)?)
|
||||
(?:\ [^;]*)?
|
||||
(?:;|$)/imx', $parent_matches[1], $result);
|
||||
$platform = null;
|
||||
$browser = null;
|
||||
$version = null;
|
||||
|
||||
$priority = array( 'Xbox One', 'Xbox', 'Windows Phone', 'Tizen', 'Android', 'FreeBSD', 'NetBSD', 'OpenBSD', 'CrOS', 'X11' );
|
||||
$return = [ PLATFORM => &$platform, BROWSER => &$browser, BROWSER_VERSION => &$version ];
|
||||
|
||||
$result['platform'] = array_unique($result['platform']);
|
||||
if( count($result['platform']) > 1 ) {
|
||||
if( $keys = array_intersect($priority, $result['platform']) ) {
|
||||
$platform = reset($keys);
|
||||
} else {
|
||||
$platform = $result['platform'][0];
|
||||
if( !$u_agent ) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
if( preg_match('/\((.*?)\)/m', $u_agent, $parent_matches) ) {
|
||||
preg_match_all(<<<'REGEX'
|
||||
/(?P<platform>BB\d+;|Android|Adr|Symbian|Sailfish|CrOS|Tizen|iPhone|iPad|iPod|Linux|(?:Open|Net|Free)BSD|Macintosh|
|
||||
Windows(?:\ Phone)?|Silk|linux-gnu|BlackBerry|PlayBook|X11|(?:New\ )?Nintendo\ (?:WiiU?|3?DS|Switch)|Xbox(?:\ One)?)
|
||||
(?:\ [^;]*)?
|
||||
(?:;|$)/imx
|
||||
REGEX
|
||||
, $parent_matches[1], $result);
|
||||
|
||||
$priority = [ 'Xbox One', 'Xbox', 'Windows Phone', 'Tizen', 'Android', 'FreeBSD', 'NetBSD', 'OpenBSD', 'CrOS', 'X11', 'Sailfish' ];
|
||||
|
||||
$result[PLATFORM] = array_unique($result[PLATFORM]);
|
||||
if( count($result[PLATFORM]) > 1 ) {
|
||||
if( $keys = array_intersect($priority, $result[PLATFORM]) ) {
|
||||
$platform = reset($keys);
|
||||
} else {
|
||||
$platform = $result[PLATFORM][0];
|
||||
}
|
||||
} elseif( isset($result[PLATFORM][0]) ) {
|
||||
$platform = $result[PLATFORM][0];
|
||||
}
|
||||
} elseif( isset($result['platform'][0]) ) {
|
||||
$platform = $result['platform'][0];
|
||||
}
|
||||
}
|
||||
|
||||
if( $platform == 'linux-gnu' || $platform == 'X11' ) {
|
||||
$platform = 'Linux';
|
||||
} elseif( $platform == 'CrOS' ) {
|
||||
$platform = 'Chrome OS';
|
||||
}
|
||||
|
||||
preg_match_all('%(?P<browser>Camino|Kindle(\ Fire)?|Firefox|Iceweasel|IceCat|Safari|MSIE|Trident|AppleWebKit|
|
||||
TizenBrowser|(?:Headless)?Chrome|YaBrowser|Vivaldi|IEMobile|Opera|OPR|Silk|Midori|Edge|Edg|CriOS|UCBrowser|Puffin|OculusBrowser|SamsungBrowser|
|
||||
Baiduspider|Googlebot|YandexBot|bingbot|Lynx|Version|Wget|curl|
|
||||
Valve\ Steam\ Tenfoot|
|
||||
NintendoBrowser|PLAYSTATION\ (\d|Vita)+)
|
||||
(?:\)?;?)
|
||||
(?:(?:[:/ ])(?P<version>[0-9A-Z.]+)|/(?:[A-Z]*))%ix',
|
||||
$u_agent, $result);
|
||||
|
||||
// If nothing matched, return null (to avoid undefined index errors)
|
||||
if( !isset($result['browser'][0]) || !isset($result['version'][0]) ) {
|
||||
if( preg_match('%^(?!Mozilla)(?P<browser>[A-Z0-9\-]+)(/(?P<version>[0-9A-Z.]+))?%ix', $u_agent, $result) ) {
|
||||
return array( 'platform' => $platform ?: null, 'browser' => $result['browser'], 'version' => isset($result['version']) ? $result['version'] ?: null : null );
|
||||
}
|
||||
|
||||
return $empty;
|
||||
}
|
||||
if( $platform == 'linux-gnu' || $platform == 'X11' ) {
|
||||
$platform = 'Linux';
|
||||
} elseif( $platform == 'CrOS' ) {
|
||||
$platform = 'Chrome OS';
|
||||
} elseif( $platform == 'Adr' ) {
|
||||
$platform = 'Android';
|
||||
} elseif( $platform === null ) {
|
||||
if(preg_match_all('%(?P<platform>Android)[:/ ]%ix', $u_agent, $result)) {
|
||||
$platform = $result[PLATFORM][0];
|
||||
}
|
||||
}
|
||||
|
||||
if( preg_match('/rv:(?P<version>[0-9A-Z.]+)/i', $u_agent, $rv_result) ) {
|
||||
$rv_result = $rv_result['version'];
|
||||
}
|
||||
preg_match_all(<<<'REGEX'
|
||||
%(?P<browser>Camino|Kindle(\ Fire)?|Firefox|Iceweasel|IceCat|Safari|MSIE|Trident|AppleWebKit|
|
||||
TizenBrowser|(?:Headless)?Chrome|YaBrowser|Vivaldi|IEMobile|Opera|OPR|Silk|Midori|(?-i:Edge)|EdgA?|CriOS|UCBrowser|Puffin|
|
||||
OculusBrowser|SamsungBrowser|SailfishBrowser|XiaoMi/MiuiBrowser|
|
||||
Baiduspider|Applebot|Facebot|Googlebot|YandexBot|bingbot|Lynx|Version|Wget|curl|
|
||||
Valve\ Steam\ Tenfoot|
|
||||
NintendoBrowser|PLAYSTATION\ (?:\d|Vita)+)
|
||||
\)?;?
|
||||
(?:[:/ ](?P<version>[0-9A-Z.]+)|/[A-Z]*)%ix
|
||||
REGEX
|
||||
, $u_agent, $result);
|
||||
|
||||
$browser = $result['browser'][0];
|
||||
$version = $result['version'][0];
|
||||
// If nothing matched, return null (to avoid undefined index errors)
|
||||
if( !isset($result[BROWSER][0], $result[BROWSER_VERSION][0]) ) {
|
||||
if( preg_match('%^(?!Mozilla)(?P<browser>[A-Z0-9\-]+)(/(?P<version>[0-9A-Z.]+))?%ix', $u_agent, $result) ) {
|
||||
return [ PLATFORM => $platform ?: null, BROWSER => $result[BROWSER], BROWSER_VERSION => empty($result[BROWSER_VERSION]) ? null : $result[BROWSER_VERSION] ];
|
||||
}
|
||||
|
||||
$lowerBrowser = array_map('strtolower', $result['browser']);
|
||||
return $return;
|
||||
}
|
||||
|
||||
$find = function ( $search, &$key, &$value = null ) use ( $lowerBrowser ) {
|
||||
$search = (array)$search;
|
||||
if( preg_match('/rv:(?P<version>[0-9A-Z.]+)/i', $u_agent, $rv_result) ) {
|
||||
$rv_result = $rv_result[BROWSER_VERSION];
|
||||
}
|
||||
|
||||
foreach( $search as $val ) {
|
||||
$xkey = array_search(strtolower($val), $lowerBrowser);
|
||||
if( $xkey !== false ) {
|
||||
$value = $val;
|
||||
$key = $xkey;
|
||||
$browser = $result[BROWSER][0];
|
||||
$version = $result[BROWSER_VERSION][0];
|
||||
|
||||
$lowerBrowser = array_map('strtolower', $result[BROWSER]);
|
||||
|
||||
$find = function ( $search, &$key = null, &$value = null ) use ( $lowerBrowser ) {
|
||||
$search = (array)$search;
|
||||
|
||||
foreach( $search as $val ) {
|
||||
$xkey = array_search(strtolower($val), $lowerBrowser);
|
||||
if( $xkey !== false ) {
|
||||
$value = $val;
|
||||
$key = $xkey;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
$findT = function ( array $search, &$key = null, &$value = null ) use ( $find ) {
|
||||
$value2 = null;
|
||||
if( $find(array_keys($search), $key, $value2) ) {
|
||||
$value = $search[$value2];
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
return false;
|
||||
};
|
||||
|
||||
$key = 0;
|
||||
$val = '';
|
||||
if( $browser == 'Iceweasel' || strtolower($browser) == 'icecat' ) {
|
||||
$browser = 'Firefox';
|
||||
} elseif( $find('Playstation Vita', $key) ) {
|
||||
$platform = 'PlayStation Vita';
|
||||
$browser = 'Browser';
|
||||
} elseif( $find(array( 'Kindle Fire', 'Silk' ), $key, $val) ) {
|
||||
$browser = $val == 'Silk' ? 'Silk' : 'Kindle';
|
||||
$platform = 'Kindle Fire';
|
||||
if( !($version = $result['version'][$key]) || !is_numeric($version[0]) ) {
|
||||
$version = $result['version'][array_search('Version', $result['browser'])];
|
||||
}
|
||||
} elseif( $find('NintendoBrowser', $key) || $platform == 'Nintendo 3DS' ) {
|
||||
$browser = 'NintendoBrowser';
|
||||
$version = $result['version'][$key];
|
||||
} elseif( $find('Kindle', $key, $platform) ) {
|
||||
$browser = $result['browser'][$key];
|
||||
$version = $result['version'][$key];
|
||||
} elseif( $find('OPR', $key) ) {
|
||||
$browser = 'Opera';
|
||||
$version = $result['version'][$key];
|
||||
} elseif( $find('Opera', $key, $browser) ) {
|
||||
$find('Version', $key);
|
||||
$version = $result['version'][$key];
|
||||
} elseif( $find('Puffin', $key, $browser) ) {
|
||||
$version = $result['version'][$key];
|
||||
if( strlen($version) > 3 ) {
|
||||
$part = substr($version, -2);
|
||||
if( ctype_upper($part) ) {
|
||||
$version = substr($version, 0, -2);
|
||||
$key = 0;
|
||||
$val = '';
|
||||
if( $findT([ 'OPR' => 'Opera', 'Facebot' => 'iMessageBot', 'UCBrowser' => 'UC Browser', 'YaBrowser' => 'Yandex', 'Iceweasel' => 'Firefox', 'Icecat' => 'Firefox', 'CriOS' => 'Chrome', 'Edg' => 'Edge', 'EdgA' => 'Edge', 'XiaoMi/MiuiBrowser' => 'MiuiBrowser' ], $key, $browser) ) {
|
||||
$version = is_numeric(substr($result[BROWSER_VERSION][$key], 0, 1)) ? $result[BROWSER_VERSION][$key] : null;
|
||||
} elseif( $find('Playstation Vita', $key, $platform) ) {
|
||||
$platform = 'PlayStation Vita';
|
||||
$browser = 'Browser';
|
||||
} elseif( $find([ 'Kindle Fire', 'Silk' ], $key, $val) ) {
|
||||
$browser = $val == 'Silk' ? 'Silk' : 'Kindle';
|
||||
$platform = 'Kindle Fire';
|
||||
if( !($version = $result[BROWSER_VERSION][$key]) || !is_numeric($version[0]) ) {
|
||||
$version = $result[BROWSER_VERSION][array_search('Version', $result[BROWSER])];
|
||||
}
|
||||
} elseif( $find('NintendoBrowser', $key) || $platform == 'Nintendo 3DS' ) {
|
||||
$browser = 'NintendoBrowser';
|
||||
$version = $result[BROWSER_VERSION][$key];
|
||||
} elseif( $find('Kindle', $key, $platform) ) {
|
||||
$browser = $result[BROWSER][$key];
|
||||
$version = $result[BROWSER_VERSION][$key];
|
||||
} elseif( $find('Opera', $key, $browser) ) {
|
||||
$find('Version', $key);
|
||||
$version = $result[BROWSER_VERSION][$key];
|
||||
} elseif( $find('Puffin', $key, $browser) ) {
|
||||
$version = $result[BROWSER_VERSION][$key];
|
||||
if( strlen($version) > 3 ) {
|
||||
$part = substr($version, -2);
|
||||
if( ctype_upper($part) ) {
|
||||
$version = substr($version, 0, -2);
|
||||
|
||||
$flags = array( 'IP' => 'iPhone', 'IT' => 'iPad', 'AP' => 'Android', 'AT' => 'Android', 'WP' => 'Windows Phone', 'WT' => 'Windows' );
|
||||
if( isset($flags[$part]) ) {
|
||||
$platform = $flags[$part];
|
||||
$flags = [ 'IP' => 'iPhone', 'IT' => 'iPad', 'AP' => 'Android', 'AT' => 'Android', 'WP' => 'Windows Phone', 'WT' => 'Windows' ];
|
||||
if( isset($flags[$part]) ) {
|
||||
$platform = $flags[$part];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif( $find('YaBrowser', $key, $browser) ) {
|
||||
$browser = 'Yandex';
|
||||
$version = $result['version'][$key];
|
||||
} elseif( $find(array( 'Edge', 'Edg' ), $key, $browser) ) {
|
||||
$browser = 'Edge';
|
||||
$version = $result['version'][$key];
|
||||
} elseif( $find(array( 'IEMobile', 'Midori', 'Vivaldi', 'OculusBrowser', 'SamsungBrowser', 'Valve Steam Tenfoot', 'Chrome', 'HeadlessChrome' ), $key, $browser) ) {
|
||||
$version = $result['version'][$key];
|
||||
} elseif( $rv_result && $find('Trident', $key) ) {
|
||||
$browser = 'MSIE';
|
||||
$version = $rv_result;
|
||||
} elseif( $find('UCBrowser', $key) ) {
|
||||
$browser = 'UC Browser';
|
||||
$version = $result['version'][$key];
|
||||
} elseif( $find('CriOS', $key) ) {
|
||||
$browser = 'Chrome';
|
||||
$version = $result['version'][$key];
|
||||
} elseif( $browser == 'AppleWebKit' ) {
|
||||
if( $platform == 'Android' ) {
|
||||
$browser = 'Android Browser';
|
||||
} elseif( strpos($platform, 'BB') === 0 ) {
|
||||
$browser = 'BlackBerry Browser';
|
||||
$platform = 'BlackBerry';
|
||||
} elseif( $platform == 'BlackBerry' || $platform == 'PlayBook' ) {
|
||||
$browser = 'BlackBerry Browser';
|
||||
} else {
|
||||
$find('Safari', $key, $browser) || $find('TizenBrowser', $key, $browser);
|
||||
} elseif( $find([ 'Applebot', 'IEMobile', 'Edge', 'Midori', 'Vivaldi', 'OculusBrowser', 'SamsungBrowser', 'Valve Steam Tenfoot', 'Chrome', 'HeadlessChrome', 'SailfishBrowser' ], $key, $browser) ) {
|
||||
$version = $result[BROWSER_VERSION][$key];
|
||||
} elseif( $rv_result && $find('Trident') ) {
|
||||
$browser = 'MSIE';
|
||||
$version = $rv_result;
|
||||
} elseif( $browser == 'AppleWebKit' ) {
|
||||
if( $platform == 'Android' ) {
|
||||
$browser = 'Android Browser';
|
||||
} elseif( strpos((string)$platform, 'BB') === 0 ) {
|
||||
$browser = 'BlackBerry Browser';
|
||||
$platform = 'BlackBerry';
|
||||
} elseif( $platform == 'BlackBerry' || $platform == 'PlayBook' ) {
|
||||
$browser = 'BlackBerry Browser';
|
||||
} else {
|
||||
$find('Safari', $key, $browser) || $find('TizenBrowser', $key, $browser);
|
||||
}
|
||||
|
||||
$find('Version', $key);
|
||||
$version = $result[BROWSER_VERSION][$key];
|
||||
} elseif( $pKey = preg_grep('/playstation \d/i', $result[BROWSER]) ) {
|
||||
$pKey = reset($pKey);
|
||||
|
||||
$platform = 'PlayStation ' . preg_replace('/\D/', '', $pKey);
|
||||
$browser = 'NetFront';
|
||||
}
|
||||
|
||||
$find('Version', $key);
|
||||
$version = $result['version'][$key];
|
||||
} elseif( $pKey = preg_grep('/playstation \d/i', $result['browser']) ) {
|
||||
$pKey = reset($pKey);
|
||||
|
||||
$platform = 'PlayStation ' . preg_replace('/\D/', '', $pKey);
|
||||
$browser = 'NetFront';
|
||||
return $return;
|
||||
}
|
||||
|
||||
return array( 'platform' => $platform ?: null, 'browser' => $browser ?: null, 'version' => $version ?: null );
|
||||
}
|
||||
@@ -72,7 +72,8 @@ class _PaginatedResponse extends Response
|
||||
public function __construct( $httpCode, $iterator, $page, $activeRecordClass, $count, \IPS\Member $authorizedMember = NULL, $perPage=NULL )
|
||||
{
|
||||
$this->httpCode = $httpCode;
|
||||
$this->page = (int) $page;
|
||||
$page = (int) $page;
|
||||
$this->page = $page > 0 ? $page : 1;
|
||||
$this->iterator = $iterator;
|
||||
$this->activeRecordClass = $activeRecordClass;
|
||||
$this->count = (int) $count;
|
||||
|
||||
@@ -786,7 +786,6 @@ EOF;
|
||||
|
||||
$classes = array();
|
||||
$jsonFile = $this->getApplicationPath() . "/data/extensions.json";
|
||||
$directory = $this->getApplicationPath() . "/extensions/{$app}/{$extension}";
|
||||
|
||||
/* New extensions.json based approach */
|
||||
if ( file_exists( $jsonFile ) and $json = @json_decode( \file_get_contents( $jsonFile ), TRUE ) )
|
||||
@@ -814,56 +813,6 @@ EOF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Legacy DirectoryIterator approach */
|
||||
elseif ( is_dir( $directory ) )
|
||||
{
|
||||
$dir = new \DirectoryIterator( $directory );
|
||||
|
||||
foreach ( $dir as $file )
|
||||
{
|
||||
/* Macs create copies of files with "._" prefix which breaks when we just load up all files in a dir, ignore those */
|
||||
if ( !$file->isDir() and !$file->isDot() and mb_substr( $file, -4 ) === '.php' AND mb_substr( $file, 0, 2 ) != '._' )
|
||||
{
|
||||
$classname = 'IPS\\' . $this->directory . '\extensions\\' . $app . '\\' . $extension . '\\' . mb_substr( $file, 0, -4 );
|
||||
|
||||
/* Check if class exists - sometimes we have to use blank files to wipe out old extensions */
|
||||
try
|
||||
{
|
||||
if( !class_exists( $classname ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( method_exists( $classname, 'deprecated' ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
catch( \ErrorException $e )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( method_exists( $classname, 'generate' ) )
|
||||
{
|
||||
$classes = array_merge( $classes, $classname::generate() );
|
||||
}
|
||||
elseif ( !$construct )
|
||||
{
|
||||
$classes[ mb_substr( $file, 0, -4 ) ] = $classname;
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
$classes[ mb_substr( $file, 0, -4 ) ] = new $classname( $checkAccess === TRUE ? \IPS\Member::loggedIn() : ( $checkAccess === FALSE ? NULL : $checkAccess ) );
|
||||
}
|
||||
catch( \RuntimeException $e ){}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $classes;
|
||||
}
|
||||
@@ -1018,7 +967,7 @@ EOF;
|
||||
);
|
||||
|
||||
/* Upgrade */
|
||||
if( !$this->protected AND !\IPS\DEMO_MODE AND $this->marketplace_id === NULL AND \IPS\IPS::canManageResources() AND \IPS\IPS::checkThirdParty() )
|
||||
if( !$this->protected AND !\IPS\DEMO_MODE AND \IPS\IPS::canManageResources() )
|
||||
{
|
||||
$buttons['upgrade'] = array(
|
||||
'icon' => 'upload',
|
||||
@@ -1054,28 +1003,25 @@ EOF;
|
||||
$buttons['edit'] = $edit;
|
||||
}
|
||||
|
||||
if( !$this->marketplace_id )
|
||||
{
|
||||
$buttons['compilejs'] = array(
|
||||
'icon' => 'cog',
|
||||
'title' => 'app_compile_js',
|
||||
'link' => \IPS\Http\Url::internal( "app=core&module=applications&controller=applications&appKey={$this->_id}&do=compilejs" )->csrf()
|
||||
);
|
||||
$buttons['compilejs'] = array(
|
||||
'icon' => 'cog',
|
||||
'title' => 'app_compile_js',
|
||||
'link' => \IPS\Http\Url::internal( "app=core&module=applications&controller=applications&appKey={$this->_id}&do=compilejs" )->csrf()
|
||||
);
|
||||
|
||||
$buttons['build'] = array(
|
||||
'icon' => 'cog',
|
||||
'title' => 'app_build',
|
||||
'link' => \IPS\Http\Url::internal("app=core&module=applications&controller=applications&appKey={$this->_id}&do=build"),
|
||||
'data' => array('ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('app_build'))
|
||||
);
|
||||
$buttons['build'] = array(
|
||||
'icon' => 'cog',
|
||||
'title' => 'app_build',
|
||||
'link' => \IPS\Http\Url::internal("app=core&module=applications&controller=applications&appKey={$this->_id}&do=build"),
|
||||
'data' => array('ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('app_build'))
|
||||
);
|
||||
|
||||
$buttons['export'] = array(
|
||||
'icon' => 'download',
|
||||
'title' => 'download',
|
||||
'link' => \IPS\Http\Url::internal("app=core&module=applications&controller=applications&appKey={$this->_id}&do=download"),
|
||||
'data' => array('ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('download'), 'ipsDialog-remoteVerify' => 'false')
|
||||
);
|
||||
}
|
||||
$buttons['export'] = array(
|
||||
'icon' => 'download',
|
||||
'title' => 'download',
|
||||
'link' => \IPS\Http\Url::internal("app=core&module=applications&controller=applications&appKey={$this->_id}&do=download"),
|
||||
'data' => array('ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('download'), 'ipsDialog-remoteVerify' => 'false')
|
||||
);
|
||||
|
||||
$buttons['developer'] = array(
|
||||
'icon' => 'cogs',
|
||||
@@ -2787,7 +2733,11 @@ EOF;
|
||||
{
|
||||
$db = \IPS\Db::i();
|
||||
$changesToMake = array();
|
||||
|
||||
|
||||
/* If member IDs are getting near the legacy mediumint limit, we need to increase it. */
|
||||
$maxMemberId = \IPS\Db::i()->select( 'max(member_id)', 'core_members' )->first();
|
||||
$enableBigInt = ( $maxMemberId > 8288607 );
|
||||
|
||||
/* Loop the tables in the schema */
|
||||
foreach( json_decode( file_get_contents( $this->getApplicationPath() . "/data/schema.json" ), TRUE ) as $tableName => $tableDefinition )
|
||||
{
|
||||
@@ -2861,7 +2811,7 @@ EOF;
|
||||
$differences[ $k ] = array( 'is' => $localDefinition['columns'][ $columnName ][ $k ], 'shouldBe' => $v );
|
||||
}
|
||||
}
|
||||
if ( isset( $differences['type'] ) and ( $differences['type']['is'] == 'MEDIUMINT' or $differences['type']['is'] == 'INT' ) and $differences['type']['shouldBe'] == 'BIGINT' )
|
||||
if ( isset( $differences['type'] ) and ( $differences['type']['is'] == 'MEDIUMINT' or $differences['type']['is'] == 'INT' ) and $differences['type']['shouldBe'] == 'BIGINT' AND !$enableBigInt )
|
||||
{
|
||||
unset( $differences['type'] );
|
||||
if ( isset( $differences['length'] ) )
|
||||
@@ -3865,7 +3815,13 @@ EOF;
|
||||
|
||||
/* End */
|
||||
$xml->endElement();
|
||||
|
||||
|
||||
/* Enforce \n line endings */
|
||||
if( mb_strtolower( mb_substr( PHP_OS, 0, 3 ) ) === 'win' )
|
||||
{
|
||||
$v = str_replace( "\r\n", "\n", $v );
|
||||
}
|
||||
|
||||
/* Note it */
|
||||
if ( !isset( $current['0'][ $k ] ) )
|
||||
{
|
||||
@@ -3912,6 +3868,12 @@ EOF;
|
||||
|
||||
/* End */
|
||||
$xml->endElement();
|
||||
|
||||
/* Enforce \n line endings */
|
||||
if( mb_strtolower( mb_substr( PHP_OS, 0, 3 ) ) === 'win' )
|
||||
{
|
||||
$v = str_replace( "\r\n", "\n", $v );
|
||||
}
|
||||
|
||||
/* Note it */
|
||||
if ( !isset( $current['1'][ $k ] ) )
|
||||
@@ -4776,6 +4738,7 @@ EOF;
|
||||
\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_item_statistics_cache', array( "cache_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 ) );
|
||||
@@ -5448,4 +5411,35 @@ EOF;
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all essential cookies which are set by all the installed apps
|
||||
* To return a list of own cookies, use the @see \IPS\Application::_getEssentialCookieNames() method.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public final static function getEssentialCookieNames(): array
|
||||
{
|
||||
if ( !isset( \IPS\Data\Store::i()->essentialCookieNames ) )
|
||||
{
|
||||
$names = [];
|
||||
foreach( static::applications() as $app )
|
||||
{
|
||||
$names = array_merge( $names, $app->_getEssentialCookieNames() );
|
||||
}
|
||||
\IPS\Data\Store::i()->essentialCookieNames = $names;
|
||||
}
|
||||
return \IPS\Data\Store::i()->essentialCookieNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of essential cookies which are set by this app.
|
||||
* Wildcards (*) can be used at the end of cookie names for PHP set cookies.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function _getEssentialCookieNames(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -129,7 +129,7 @@ class _ItemController extends \IPS\Api\Controller
|
||||
}
|
||||
else
|
||||
{
|
||||
$where[] = array( $class::$databasePrefix . $class::$databaseColumnMap['poll'] . "=0" );
|
||||
$where[] = array( $class::$databasePrefix . $class::$databaseColumnMap['poll'] . " IS NULL" );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -240,7 +240,10 @@ abstract class _Comment extends \IPS\Content
|
||||
if ( !$obj->hidden() and ( !$first or !$item::$firstCommentRequired ) )
|
||||
{
|
||||
$obj->sendNotifications();
|
||||
$member->achievementAction( 'core', 'Comment', $obj );
|
||||
if( !$item instanceof \IPS\core\Messenger\Conversation )
|
||||
{
|
||||
$member->achievementAction( 'core', 'Comment', $obj );
|
||||
}
|
||||
}
|
||||
else if( $obj->hidden() === 1 )
|
||||
{
|
||||
@@ -467,7 +470,7 @@ abstract class _Comment extends \IPS\Content
|
||||
{
|
||||
\IPS\Redis::i()->zIncrBy( 'trending', time(), $class . '__' . $item->$itemIdColumn );
|
||||
}
|
||||
catch( \RedisException $e ) { }
|
||||
catch( \BadMethodCallException | \RedisException $e ) { }
|
||||
}
|
||||
|
||||
/* Was it moderated? Let's see why. */
|
||||
@@ -626,7 +629,7 @@ abstract class _Comment extends \IPS\Content
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if ( $member->member_id )
|
||||
if ( $member->member_id AND $this->canView( $member ) )
|
||||
{
|
||||
$item = $this->item();
|
||||
|
||||
@@ -1112,6 +1115,9 @@ abstract class _Comment extends \IPS\Content
|
||||
{
|
||||
\IPS\core\IndexNow::addUrlsToQueue( $noIndexUrls );
|
||||
}
|
||||
|
||||
/* Remove from Hive */
|
||||
\IPS\core\Hive::i()->removeContent( $this );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1264,7 +1270,7 @@ abstract class _Comment extends \IPS\Content
|
||||
/* Update last comment/review data for container and item */
|
||||
try
|
||||
{
|
||||
if ( $item !== NULL AND \in_array( 'IPS\Content\Review', class_parents( \get_called_class() ) ) )
|
||||
if ( $item !== NULL AND $item->container() !== NULL AND \in_array( 'IPS\Content\Review', class_parents( \get_called_class() ) ) )
|
||||
{
|
||||
if ( $item->container()->_reviews !== NULL )
|
||||
{
|
||||
@@ -1300,7 +1306,7 @@ abstract class _Comment extends \IPS\Content
|
||||
}
|
||||
|
||||
/* Update mappings */
|
||||
if ( \IPS\IPS::classUsesTrait( $item->container(), 'IPS\Node\Statistics' ) )
|
||||
if ( $item !== NULL AND $item->container() !== NULL AND \IPS\IPS::classUsesTrait( $item->container(), 'IPS\Node\Statistics' ) )
|
||||
{
|
||||
$item->container()->rebuildPostedIn( array( $item->$itemIdColumn ), array( $this->author() ) );
|
||||
}
|
||||
@@ -1556,6 +1562,12 @@ abstract class _Comment extends \IPS\Content
|
||||
}
|
||||
}
|
||||
|
||||
/* Finally, we do not want posts from future items */
|
||||
if ( \in_array( 'IPS\Content\FuturePublishing', class_implements( $itemClass ) ) )
|
||||
{
|
||||
$itemWhere[] = array( $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['is_future_entry'] . '=0' );
|
||||
}
|
||||
|
||||
if ( $joinContainer AND isset( $itemClass::$containerNodeClass ) )
|
||||
{
|
||||
$containerClass = $itemClass::$containerNodeClass;
|
||||
|
||||
@@ -411,6 +411,9 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
}
|
||||
catch( \OutOfRangeException $e ) { }
|
||||
|
||||
/* Remove from Hive */
|
||||
\IPS\core\Hive::i()->removeContent( $this );
|
||||
|
||||
parent::delete();
|
||||
|
||||
$this->expireWidgetCaches();
|
||||
@@ -1210,10 +1213,13 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
}
|
||||
}
|
||||
|
||||
\IPS\core\Approval::loadFromContent( \get_called_class(), $this->$idColumn )->delete();
|
||||
\IPS\core\Approval::loadFromContent( \get_called_class(), $this->$idColumn )->delete();
|
||||
}
|
||||
catch( \BadMethodCallException $e ) {}
|
||||
catch( \OutOfRangeException $e ) {}
|
||||
|
||||
/* Remove from Hive */
|
||||
\IPS\core\Hive::i()->removeContent( $this );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1387,6 +1393,9 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
\IPS\core\Approval::loadFromContent( \get_called_class(), $this->$idColumn )->delete();
|
||||
}
|
||||
catch( \OutOfRangeException $e ) { }
|
||||
|
||||
/* Remove from Hive */
|
||||
\IPS\core\Hive::i()->removeContent( $this );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1876,7 +1885,22 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
foreach ( \IPS\Db::i()->select( '*', 'core_members', ( \count( $moderators['m'] ) ? \IPS\Db::i()->in( 'member_id', $moderators['m'] ) . ' OR ' : '' ) . \IPS\Db::i()->in( 'member_group_id', $moderators['g'] ) . ' OR ' . \IPS\Db::i()->findInSet( 'mgroup_others', $moderators['g'] ) ) as $mem )
|
||||
{
|
||||
$memberObj = \IPS\Member::constructFromData( $mem );
|
||||
if( $this->canView( $memberObj ) )
|
||||
|
||||
/* Members may have individual member level mod permissions, but are also in a group that has moderator permissions. In this case, the member level restrictions always win so we need to recheck those now that we have a member object. See \IPS\Member::modPermissions(). */
|
||||
$perms = $memberObj->modPermissions();
|
||||
$canView = $this->canView( $memberObj );
|
||||
if ( $canView === TRUE and $container = $item->containerWrapper() and isset( $container::$modPerm ) )
|
||||
{
|
||||
if ( isset( $perms[ $container::$modPerm ] ) and $perms[ $container::$modPerm ] != '*' and $perms[ $container::$modPerm ] != -1 )
|
||||
{
|
||||
if ( empty( $perms[ $container::$modPerm ] ) or ! in_array( $item->mapped('container'), $perms[ $container::$modPerm ] ) )
|
||||
{
|
||||
$canView = FALSE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( $canView === TRUE )
|
||||
{
|
||||
$notification->recipients->attach( $memberObj );
|
||||
}
|
||||
@@ -2465,7 +2489,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
}
|
||||
|
||||
/* Queue if there's lots, or just send them */
|
||||
if ( $count > static::NOTIFICATIONS_PER_BATCH )
|
||||
if ( $count > \IPS\NOTIFICATION_BACKGROUND_THRESHOLD )
|
||||
{
|
||||
$idColumn = $this::$databaseColumnId;
|
||||
\IPS\Task::queue( 'core', 'Follow', array( 'class' => \get_class( $this ), 'item' => $this->$idColumn, 'sentTo' => $sentTo, 'followerCount' => $count ), 2 );
|
||||
@@ -3030,11 +3054,12 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
/**
|
||||
* Log keyword usage, if any
|
||||
*
|
||||
* @param string $content Content/text of submission
|
||||
* @param string|NULL $title Title of submission
|
||||
* @param string $content Content/text of submission
|
||||
* @param string|NULL $title Title of submission
|
||||
* @param int|NULL $date Date of submission
|
||||
* @return void
|
||||
*/
|
||||
public function checkKeywords( $content, $title=NULL )
|
||||
public function checkKeywords( $content, $title=NULL, ?int $date = NULL )
|
||||
{
|
||||
/* Do we have any keywords to track? */
|
||||
if( !\IPS\Settings::i()->stats_keywords )
|
||||
@@ -3078,7 +3103,8 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
if( \in_array( $keyword, $words ) )
|
||||
{
|
||||
\IPS\Db::i()->insert( 'core_statistics', array( 'time' => time(), 'type' => 'keyword', 'value_4' => $keyword, 'extra_data' => $extraData ) );
|
||||
$date = $date ?: \IPS\DateTime::create()->getTimestamp();
|
||||
\IPS\Db::i()->insert( 'core_statistics', array( 'time' => $date, 'type' => 'keyword', 'value_4' => $keyword, 'extra_data' => $extraData ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3149,6 +3175,12 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
if ( $this instanceof \IPS\Content\EditHistory and $this->mapped('edit_time') and ( $this->mapped('edit_show') or \IPS\Member::loggedIn()->modPermission('can_view_editlog') ) and \IPS\Settings::i()->edit_log )
|
||||
{
|
||||
/* If anonymous and we can't see, it's a no. */
|
||||
if ( $this->isAnonymous() AND !\IPS\Member::loggedIn()->modPermission('can_view_anonymous_posters') )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$template = static::$editLineTemplate[1];
|
||||
return \IPS\Theme::i()->getTemplate( static::$editLineTemplate[0][0], static::$editLineTemplate[0][1], ( isset( static::$editLineTemplate[0][2] ) ) ? static::$editLineTemplate[0][2] : NULL )->$template( $this, ( isset( static::$databaseColumnMap['edit_reason'] ) and $this->mapped('edit_reason') ) );
|
||||
}
|
||||
@@ -3287,4 +3319,5 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
+134
-49
@@ -104,6 +104,16 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
catch( \OutOfRangeException $e ) { }
|
||||
}
|
||||
}
|
||||
|
||||
if( is_subclass_of( $class, 'IPS\Content\FuturePublishing' ) and isset( $class::$databaseColumnMap['is_future_entry'] ) AND isset( $class::$databaseColumnMap['future_date'] ) )
|
||||
{
|
||||
$futureColumn = $class::$databaseColumnMap['future_date'];
|
||||
$futureEntry = $class::$databaseColumnMap['is_future_entry'];
|
||||
if ( $item->$futureEntry == 1 and $item->$futureColumn <= time() )
|
||||
{
|
||||
$item->publish();
|
||||
}
|
||||
}
|
||||
|
||||
/* If this is an AJAX request (like the topic hovercard), we don't want to do any of the below like update views and mark read */
|
||||
if ( \IPS\Request::i()->isAjax() and !$this->updateViewsAndMarkersOnAjax )
|
||||
@@ -117,27 +127,33 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
/* We also want to update the views if we have a page parameter */
|
||||
if( $item instanceof \IPS\Content\ReadMarkers AND isset( \IPS\Request::i()->page ) AND \IPS\Request::i()->page )
|
||||
{
|
||||
|
||||
$idColumn = $class::$databaseColumnId;
|
||||
if ( \in_array( 'IPS\Content\Views', class_implements( $class ) ) AND isset( $class::$databaseColumnMap['views'] ) )
|
||||
if ( \IPS\IPS::classUsesTrait( $class, 'IPS\Content\ViewUpdates' ) )
|
||||
{
|
||||
$countUpdated = false;
|
||||
if ( \IPS\REDIS_ENABLED and \IPS\CACHE_METHOD == 'Redis' and ( \IPS\CACHE_CONFIG or \IPS\REDIS_CONFIG ) )
|
||||
$item->updateViews();
|
||||
}
|
||||
else
|
||||
{
|
||||
$idColumn = $class::$databaseColumnId;
|
||||
if ( \in_array( 'IPS\Content\Views', class_implements( $class ) ) AND isset( $class::$databaseColumnMap['views'] ) )
|
||||
{
|
||||
try
|
||||
$countUpdated = false;
|
||||
if ( \IPS\REDIS_ENABLED and \IPS\CACHE_METHOD == 'Redis' and ( \IPS\CACHE_CONFIG or \IPS\REDIS_CONFIG ) )
|
||||
{
|
||||
\IPS\Redis::i()->zIncrBy( 'topic_views', 1, $class .'__' . $item->$idColumn );
|
||||
$countUpdated = true;
|
||||
try
|
||||
{
|
||||
\IPS\Redis::i()->zIncrBy( 'topic_views', 1, $class .'__' . $item->$idColumn );
|
||||
$countUpdated = true;
|
||||
}
|
||||
catch( \Exception $e ) {}
|
||||
}
|
||||
|
||||
if ( ! $countUpdated )
|
||||
{
|
||||
\IPS\Db::i()->insert( 'core_view_updates', array(
|
||||
'classname' => $class,
|
||||
'id' => $item->$idColumn
|
||||
) );
|
||||
}
|
||||
catch( \Exception $e ) {}
|
||||
}
|
||||
|
||||
if ( ! $countUpdated )
|
||||
{
|
||||
\IPS\Db::i()->insert( 'core_view_updates', array(
|
||||
'classname' => $class,
|
||||
'id' => $item->$idColumn
|
||||
) );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,36 +247,43 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
|
||||
/* Update Views */
|
||||
$idColumn = $class::$databaseColumnId;
|
||||
if ( \in_array( 'IPS\Content\Views', class_implements( $class ) ) AND isset( $class::$databaseColumnMap['views'] ) )
|
||||
if ( \IPS\IPS::classUsesTrait( $class, 'IPS\Content\ViewUpdates' ) )
|
||||
{
|
||||
$countUpdated = false;
|
||||
if ( \IPS\REDIS_ENABLED and \IPS\CACHE_METHOD == 'Redis' and ( \IPS\CACHE_CONFIG or \IPS\REDIS_CONFIG ) )
|
||||
$item->updateViews();
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( \in_array( 'IPS\Content\Views', class_implements( $class ) ) AND isset( $class::$databaseColumnMap['views'] ) )
|
||||
{
|
||||
try
|
||||
$countUpdated = false;
|
||||
if ( \IPS\REDIS_ENABLED and \IPS\CACHE_METHOD == 'Redis' and ( \IPS\CACHE_CONFIG or \IPS\REDIS_CONFIG ) )
|
||||
{
|
||||
\IPS\Redis::i()->zIncrBy( 'topic_views', 1, $class .'__' . $item->$idColumn );
|
||||
|
||||
$countUpdated = true;
|
||||
try
|
||||
{
|
||||
\IPS\Redis::i()->zIncrBy( 'topic_views', 1, $class .'__' . $item->$idColumn );
|
||||
|
||||
$countUpdated = true;
|
||||
}
|
||||
catch( \Exception $e ) {}
|
||||
}
|
||||
catch( \Exception $e ) {}
|
||||
}
|
||||
|
||||
if( \IPS\Application::appIsEnabled( 'cloud' ) and \IPS\cloud\Realtime::i()->isEnabled('trending') and $class::$includeInTrending )
|
||||
{
|
||||
try
|
||||
|
||||
if( \IPS\Application::appIsEnabled( 'cloud' ) and \IPS\cloud\Realtime::i()->isEnabled('trending') and $class::$includeInTrending )
|
||||
{
|
||||
/* Score by timestamp for trending */
|
||||
\IPS\Redis::i()->zIncrBy( 'trending', time() * 0.1, $class .'__' . $item->$idColumn );
|
||||
try
|
||||
{
|
||||
/* Score by timestamp for trending */
|
||||
\IPS\Redis::i()->zIncrBy( 'trending', time() * 0.1, $class .'__' . $item->$idColumn );
|
||||
}
|
||||
catch( \BadMethodCallException | \RedisException $e ) {}
|
||||
}
|
||||
|
||||
if ( ! $countUpdated )
|
||||
{
|
||||
\IPS\Db::i()->insert( 'core_view_updates', array(
|
||||
'classname' => $class,
|
||||
'id' => $item->$idColumn
|
||||
) );
|
||||
}
|
||||
catch( \RedisException $e ) {}
|
||||
}
|
||||
|
||||
if ( ! $countUpdated )
|
||||
{
|
||||
\IPS\Db::i()->insert( 'core_view_updates', array(
|
||||
'classname' => $class,
|
||||
'id' => $item->$idColumn
|
||||
) );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,7 +384,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
\IPS\Output::i()->redirect( $class::getRedirectFrom( \IPS\Request::i()->id )->url(), '', 301 );
|
||||
}
|
||||
catch( \OutOfRangeException $e ) { }
|
||||
|
||||
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
@@ -1000,6 +1023,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
}
|
||||
else
|
||||
{
|
||||
\IPS\Output::i()->bypassCsrfKeyCheck = TRUE;
|
||||
\IPS\Output::i()->output = $form->customTemplate( array( \IPS\Theme::i()->getTemplate( 'forms', 'core' ), 'popupTemplate' ) );
|
||||
return;
|
||||
}
|
||||
@@ -1628,7 +1652,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* Moderator Log
|
||||
* Moderation Log
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
@@ -1643,11 +1667,65 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
{
|
||||
$class = static::$contentModel;
|
||||
$item = $class::loadAndCheckPerms( \IPS\Request::i()->id );
|
||||
|
||||
|
||||
/* Set up some stuff so we're not doing too much logic / assignment in the template */
|
||||
$modlog = $item->moderationTable();
|
||||
|
||||
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->modLog( $item, $modlog );
|
||||
}
|
||||
catch ( \OutOfRangeException $e )
|
||||
{
|
||||
\IPS\Output::i()->error( 'node_error', '2S136/T', 404, '' );
|
||||
}
|
||||
catch( \DomainException $e )
|
||||
{
|
||||
\IPS\Output::i()->error( 'no_module_permission', '2S136/U', 403, '' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moderation Log
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function analytics()
|
||||
{
|
||||
if( !\IPS\Member::loggedIn()->modPermission( 'can_view_moderation_log' ) )
|
||||
{
|
||||
\IPS\Output::i()->error( 'no_module_permission', '2S136/1F', 403, '' );
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$class = static::$contentModel;
|
||||
$item = $class::loadAndCheckPerms( \IPS\Request::i()->id );
|
||||
|
||||
/* Set up some stuff so we're not doing too much logic / assignment in the template */
|
||||
$lastCommenter = $members = $busy = $reacted = $images = NULL;
|
||||
try
|
||||
{
|
||||
$lastCommenter = $item->lastCommenter();
|
||||
if ( !$lastCommenter->member_id )
|
||||
{
|
||||
$lastCommenter = NULL;
|
||||
}
|
||||
}
|
||||
catch( \BadMethodCallException $e ) { }
|
||||
|
||||
if ( \IPS\IPS::classUsesTrait( $item, 'IPS\Content\Statistics' ) )
|
||||
{
|
||||
$members = $item->topPosters();
|
||||
$busy = $item->popularDays();
|
||||
$reacted = $item->topReactedPosts();
|
||||
$images = $item->imageAttachments();
|
||||
}
|
||||
|
||||
/* Set navigation */
|
||||
$this->_setBreadcrumbAndTitle( $item );
|
||||
|
||||
\IPS\Output::i()->output = $item->moderationTable();
|
||||
\IPS\Output::i()->breadcrumb[] = array( NULL, \IPS\Member::loggedIn()->language()->addToStack( 'analytics_and_stats' ) );
|
||||
|
||||
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->analytics( $item, $lastCommenter, $members, $busy, $reacted, $images );
|
||||
}
|
||||
catch ( \OutOfRangeException $e )
|
||||
{
|
||||
@@ -1967,9 +2045,11 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
|
||||
if ( $values = $form->values() )
|
||||
{
|
||||
$review->author_response = $values['reviewResponse'];
|
||||
$review->save();
|
||||
|
||||
$review->setResponse( $values['reviewResponse'] );
|
||||
|
||||
/* Claim attachments and clear the editor */
|
||||
\IPS\File::claimAttachments( 'reviewResponse-' . $class::$application . '/' . $class::$module . '-' . \IPS\Request::i()->review, \IPS\Request::i()->id, \IPS\Request::i()->review, \get_class( $item ) );
|
||||
|
||||
\IPS\Output::i()->redirect( $review->url() );
|
||||
}
|
||||
|
||||
@@ -4008,9 +4088,14 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$class = static::$contentModel;
|
||||
if( !\is_subclass_of( $class, 'IPS\Content\Item' ) )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$class = static::$contentModel;
|
||||
$item = $class::loadAndCheckPerms( \IPS\Request::i()->id );
|
||||
$items = [];
|
||||
|
||||
|
||||
+110
-52
@@ -41,6 +41,13 @@ abstract class _Item extends \IPS\Content
|
||||
* @brief [Content\Item] Follower count
|
||||
*/
|
||||
public $followerCount = NULL;
|
||||
|
||||
/**
|
||||
* Should IndexNow be skipped for this item? Can be used to prevent that Private Messages,
|
||||
* Reports and other content which is never going to be visible to guests is triggering the requests.
|
||||
* @var bool
|
||||
*/
|
||||
public static bool $skipIndexNow = FALSE;
|
||||
|
||||
/**
|
||||
* @brief [Content\Item] If $firstCommentRequired is TRUE, when comments are split from an item or items are merged, the author
|
||||
@@ -521,7 +528,7 @@ abstract class _Item extends \IPS\Content
|
||||
}
|
||||
|
||||
/* Dish out points */
|
||||
if ( !$obj->isFutureDate() and !$obj->hidden() )
|
||||
if ( !$obj->isFutureDate() and !$obj->hidden() and !$obj instanceof \IPS\core\Messenger\Conversation )
|
||||
{
|
||||
$member->achievementAction( 'core', 'NewContentItem', $obj );
|
||||
}
|
||||
@@ -619,6 +626,12 @@ abstract class _Item extends \IPS\Content
|
||||
{
|
||||
$column = static::$databaseColumnMap['future_date'];
|
||||
$this->$column = $time->getTimestamp();
|
||||
|
||||
if( isset( static::$databaseColumnMap['is_future_entry'] ) )
|
||||
{
|
||||
$column = static::$databaseColumnMap['is_future_entry'];
|
||||
$this->$column = $time->getTimestamp() > time();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -718,7 +731,12 @@ abstract class _Item extends \IPS\Content
|
||||
/* Title */
|
||||
if ( isset( static::$databaseColumnMap['title'] ) )
|
||||
{
|
||||
$return['title'] = new \IPS\Helpers\Form\Text( static::$formLangPrefix . 'title', isset( \IPS\Request::i()->title ) ? \IPS\Request::i()->title : $item?->mapped( 'title' ), TRUE, array( 'maxLength' => \IPS\Settings::i()->max_title_length ?: 255, 'bypassProfanity' => \IPS\Helpers\Form\Text::BYPASS_PROFANITY_SWAP ) );
|
||||
$return['title'] = new \IPS\Helpers\Form\Text( static::$formLangPrefix . 'title', isset( \IPS\Request::i()->title ) ? \IPS\Request::i()->title : $item?->mapped( 'title' ), TRUE, array( 'maxLength' => \IPS\Settings::i()->max_title_length ?: 255, 'bypassProfanity' => \IPS\Helpers\Form\Text::BYPASS_PROFANITY_SWAP ), function( $val ) {
|
||||
if ( !\IPS\Member::loggedIn()->group['g_bypass_badwords'] AND $word = \IPS\core\Profanity::checkProfanityBlocks( $val ) )
|
||||
{
|
||||
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( "form_err_word_blocked", FALSE, array( "sprintf" => array( $word ) ) ) );
|
||||
}
|
||||
} );
|
||||
$return['title']->rowClasses[] = 'ipsFieldRow_primary';
|
||||
$return['title']->rowClasses[] = 'ipsFieldRow_fullWidth';
|
||||
}
|
||||
@@ -1015,6 +1033,10 @@ abstract class _Item extends \IPS\Content
|
||||
{
|
||||
$options['autocomplete']['filterProfanity'] = TRUE;
|
||||
}
|
||||
if ( \IPS\Settings::i()->tags_alphabetical )
|
||||
{
|
||||
$options['autocomplete']['alphabetical'] = TRUE;
|
||||
}
|
||||
|
||||
$options['autocomplete']['prefix'] = static::canPrefix( NULL, $container );
|
||||
$options['autocomplete']['disallowedCharacters'] = array( '#' ); // @todo Pending \IPS\Http\Url rework, hashes cannot be used in URLs
|
||||
@@ -1149,7 +1171,7 @@ abstract class _Item extends \IPS\Content
|
||||
}
|
||||
|
||||
/* Send this URL to IndexNow if the guest can view it */
|
||||
if ( $this->canView( new \IPS\Member ) )
|
||||
if ( $this->canView( new \IPS\Member ) AND !static::$skipIndexNow )
|
||||
{
|
||||
\IPS\core\IndexNow::addUrlToQueue( $this->url() );
|
||||
}
|
||||
@@ -1330,6 +1352,14 @@ abstract class _Item extends \IPS\Content
|
||||
$this->publish();
|
||||
}
|
||||
}
|
||||
else if ( $container and ! $this->$column )
|
||||
{
|
||||
if ( $values[ static::$formLangPrefix . 'date' ] instanceof \IPS\DateTime AND $values[ static::$formLangPrefix . 'date' ]->getTimestamp() > time() )
|
||||
{
|
||||
/* Was not future, now is */
|
||||
$this->unpublish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Post anonymously */
|
||||
@@ -1903,7 +1933,7 @@ abstract class _Item extends \IPS\Content
|
||||
}
|
||||
}
|
||||
|
||||
if ( $this instanceof \IPS\Content\Views )
|
||||
if ( \IPS\IPS::classUsesTrait( $this, 'IPS\Content\ViewUpdates' ) )
|
||||
{
|
||||
$return['num_views'] = (int) $this->mapped('views');
|
||||
}
|
||||
@@ -2129,6 +2159,9 @@ abstract class _Item extends \IPS\Content
|
||||
{
|
||||
$link->markRead();
|
||||
}
|
||||
|
||||
/* Remove from Hive */
|
||||
\IPS\core\Hive::i()->removeContent( $this );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2275,7 +2308,7 @@ abstract class _Item extends \IPS\Content
|
||||
}
|
||||
|
||||
/* Merge view counts */
|
||||
if ( $this instanceof \IPS\Content\Views )
|
||||
if ( \IPS\IPS::classUsesTrait( $this, 'IPS\Content\ViewUpdates' ) )
|
||||
{
|
||||
$views += $item->mapped('views');
|
||||
\IPS\Db::i()->update( 'core_view_updates', array( 'id' => $this->$idColumn ), array( 'classname=? and id=?', (string) \get_class( $item ), $item->$idColumn ) );
|
||||
@@ -2725,7 +2758,7 @@ abstract class _Item extends \IPS\Content
|
||||
}
|
||||
|
||||
$idColumn = static::$databaseColumnId;
|
||||
|
||||
|
||||
/* Don't do anything for shadow items */
|
||||
if ( isset( static::$databaseColumnMap['moved_to'] ) )
|
||||
{
|
||||
@@ -2740,19 +2773,19 @@ abstract class _Item extends \IPS\Content
|
||||
}
|
||||
|
||||
\IPS\Db::i()->delete( 'core_item_member_map', array( 'map_class=? and map_item_id=?', (string) \get_class( $this ), (int) $this->$idColumn ) );
|
||||
|
||||
|
||||
/* Remove any meta data */
|
||||
if ( ( $this instanceof \IPS\Content\MetaData ) AND isset( static::$databaseColumnMap['meta_data'] ) )
|
||||
{
|
||||
$this->deleteAllMeta();
|
||||
}
|
||||
|
||||
|
||||
/* Unclaim attachments */
|
||||
$this->unclaimAttachments();
|
||||
|
||||
/* Delete it from the database */
|
||||
parent::delete();
|
||||
|
||||
|
||||
/* Update count */
|
||||
try
|
||||
{
|
||||
@@ -2772,7 +2805,7 @@ abstract class _Item extends \IPS\Content
|
||||
}
|
||||
}
|
||||
} catch ( \BadMethodCallException $e ) {}
|
||||
|
||||
|
||||
/* Delete comments */
|
||||
if ( isset( static::$commentClass ) )
|
||||
{
|
||||
@@ -2783,7 +2816,7 @@ abstract class _Item extends \IPS\Content
|
||||
{
|
||||
$where = $commentClass::deleteWhereSql( $this->$idColumn );
|
||||
}
|
||||
|
||||
|
||||
/* Remove any deletion logs for comments */
|
||||
$commentIds = array();
|
||||
$commentIdColumn = $commentClass::$databasePrefix . $commentClass::$databaseColumnId;
|
||||
@@ -2791,14 +2824,14 @@ abstract class _Item extends \IPS\Content
|
||||
{
|
||||
$commentIds[] = $commentId;
|
||||
}
|
||||
|
||||
|
||||
$this->deleteCommentOrReviewData( $commentIds, $commentClass );
|
||||
|
||||
if ( \is_array( $where ) and \count( $where ) )
|
||||
{
|
||||
\IPS\Db::i()->delete( $commentClass::$databaseTable, $where );
|
||||
}
|
||||
|
||||
|
||||
if( !$this->skipContainerRebuild )
|
||||
{
|
||||
try
|
||||
@@ -2810,7 +2843,7 @@ abstract class _Item extends \IPS\Content
|
||||
{
|
||||
$this->container()->_comments = ( $this->container()->_comments - $this->mapped('num_comments') );
|
||||
}
|
||||
|
||||
|
||||
$this->container()->setLastComment();
|
||||
}
|
||||
if ( $this->container()->_unapprovedComments !== NULL )
|
||||
@@ -2821,7 +2854,7 @@ abstract class _Item extends \IPS\Content
|
||||
} catch ( \BadMethodCallException $e ) {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Delete reviews */
|
||||
if ( isset( static::$reviewClass ) )
|
||||
{
|
||||
@@ -2832,7 +2865,7 @@ abstract class _Item extends \IPS\Content
|
||||
{
|
||||
$where = $reviewClass::deleteWhereSql( $this->$idColumn );
|
||||
}
|
||||
|
||||
|
||||
/* Remove any deletion logs for reviews */
|
||||
$reviewIds = array();
|
||||
$reviewIdColumn = $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['item'];
|
||||
@@ -2844,7 +2877,7 @@ abstract class _Item extends \IPS\Content
|
||||
$this->deleteCommentOrReviewData( $reviewIds, $reviewClass );
|
||||
|
||||
\IPS\Db::i()->delete( $reviewClass::$databaseTable, $where );
|
||||
|
||||
|
||||
if( !$this->skipContainerRebuild )
|
||||
{
|
||||
try
|
||||
@@ -2856,7 +2889,7 @@ abstract class _Item extends \IPS\Content
|
||||
{
|
||||
$this->container()->_reviews = ( $this->container()->_reviews - $this->mapped('num_reviews') );
|
||||
}
|
||||
|
||||
|
||||
$this->container()->setLastReview();
|
||||
}
|
||||
if ( $this->container()->_unapprovedReviews !== NULL )
|
||||
@@ -2867,7 +2900,7 @@ abstract class _Item extends \IPS\Content
|
||||
} catch ( \BadMethodCallException $e ) {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Delete tags */
|
||||
if ( $this instanceof \IPS\Content\Tags )
|
||||
{
|
||||
@@ -2876,7 +2909,7 @@ abstract class _Item extends \IPS\Content
|
||||
\IPS\Db::i()->delete( 'core_tags_cache', array( 'tag_cache_key=?', $aaiLookup ) );
|
||||
\IPS\Db::i()->delete( 'core_tags_perms', array( 'tag_perm_aai_lookup=?', $aaiLookup ) );
|
||||
}
|
||||
|
||||
|
||||
/* Delete follows */
|
||||
if ( $this instanceof \IPS\Content\Followable )
|
||||
{
|
||||
@@ -2884,7 +2917,7 @@ abstract class _Item extends \IPS\Content
|
||||
\IPS\Db::i()->delete( 'core_follow', array( 'follow_app=? AND follow_area=? AND follow_rel_id=?', static::$application, $followArea, (int) $this->$idColumn ) );
|
||||
\IPS\Db::i()->delete( 'core_follow_count_cache', array( 'class=? AND id=?', \get_called_class(), (int) $this->$idColumn ) );
|
||||
}
|
||||
|
||||
|
||||
/* Remove Notifications */
|
||||
$memberIds = array();
|
||||
|
||||
@@ -2894,13 +2927,13 @@ abstract class _Item extends \IPS\Content
|
||||
}
|
||||
|
||||
\IPS\Db::i()->delete( 'core_notifications', array( 'item_class=? AND item_id=?', (string) \get_class( $this ), (int) $this->$idColumn ) );
|
||||
|
||||
|
||||
/* Delete from Our Picks */
|
||||
\IPS\Db::i()->delete( 'core_social_promote', array( 'promote_class=? AND promote_class_id=?', (string) \get_class( $this ), (int) $this->$idColumn ) );
|
||||
|
||||
|
||||
/* Delete from redirect links */
|
||||
\IPS\Db::i()->delete( 'core_item_redirect', array( 'redirect_class=? AND redirect_new_item_id=?', (string) \get_class( $this ), (int) $this->$idColumn ) );
|
||||
|
||||
|
||||
/* Delete Polls */
|
||||
if ( $this instanceof \IPS\Content\Polls and $this->getPoll() )
|
||||
{
|
||||
@@ -2912,19 +2945,21 @@ abstract class _Item extends \IPS\Content
|
||||
{
|
||||
\IPS\Db::i()->delete( 'core_ratings', array( 'class=? AND item_id=?', \get_called_class(), $this->$idColumn ) );
|
||||
}
|
||||
|
||||
|
||||
foreach( $memberIds as $member )
|
||||
{
|
||||
\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
|
||||
if( !static::$skipIndexNow )
|
||||
{
|
||||
\IPS\core\IndexNow::addUrlToQueue( $this->url() );
|
||||
try
|
||||
{
|
||||
\IPS\core\IndexNow::addUrlToQueue( $this->url() );
|
||||
}
|
||||
catch( \LogicException $e ) {}
|
||||
}
|
||||
catch( \LogicException $e ) {}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3174,7 +3209,7 @@ abstract class _Item extends \IPS\Content
|
||||
if ( isset( static::$databaseColumnMap['approved'] ) )
|
||||
{
|
||||
$col = static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['approved'];
|
||||
if ( $member->member_id and $includeHiddenItems !== \IPS\Content\Hideable::FILTER_PUBLIC_ONLY )
|
||||
if ( $member->member_id and $includeHiddenItems !== \IPS\Content\Hideable::FILTER_PUBLIC_ONLY and isset( static::$databaseColumnMap['author'] ) )
|
||||
{
|
||||
/* Only fetching a count, for a single container with a item cache count, no future publishing, and there is only one where clause (the container limitation) */
|
||||
if( $countOnly === TRUE AND $skipPermission instanceof \IPS\Node\Model AND $skipPermission->_items !== NULL AND !\in_array( 'IPS\Content\FuturePublishing', class_implements( \get_called_class() ) ) AND \count( $where ) === 1 )
|
||||
@@ -8069,7 +8104,13 @@ abstract class _Item extends \IPS\Content
|
||||
*/
|
||||
public static function definedTags( \IPS\Node\Model $container = NULL )
|
||||
{
|
||||
return \IPS\Settings::i()->tags_predefined ? explode( ',', \IPS\Settings::i()->tags_predefined ) : array();
|
||||
$return = \IPS\Settings::i()->tags_predefined ? explode( ',', \IPS\Settings::i()->tags_predefined ) : array();
|
||||
if ( \IPS\Settings::i()->tags_alphabetical )
|
||||
{
|
||||
natcasesort( $return );
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -8125,8 +8166,20 @@ abstract class _Item extends \IPS\Content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ( isset ( $this->tags['tags'] ) ? $this->tags['tags'] : [] );
|
||||
|
||||
if ( isset( $this->tags['tags'] ) )
|
||||
{
|
||||
if ( \IPS\Settings::i()->tags_alphabetical )
|
||||
{
|
||||
natcasesort( $this->tags['tags'] );
|
||||
}
|
||||
|
||||
return $this->tags['tags'];
|
||||
}
|
||||
else
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -8617,8 +8670,29 @@ abstract class _Item extends \IPS\Content
|
||||
$this->$future = 0;
|
||||
}
|
||||
|
||||
if ( isset( static::$databaseColumnMap['last_comment'] ) or isset( static::$databaseColumnMap['last_review'] ) )
|
||||
{
|
||||
$lastCommentField = static::$databaseColumnMap['last_comment'];
|
||||
if ( \is_array( $lastCommentField ) )
|
||||
{
|
||||
foreach ( $lastCommentField as $column )
|
||||
{
|
||||
$this->$column = time();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->$lastCommentField = time();
|
||||
}
|
||||
|
||||
if ( isset( static::$databaseColumnMap['last_review'] ) )
|
||||
{
|
||||
$lastReviewField = static::$databaseColumnMap['last_review'];
|
||||
$this->$lastReviewField = time();
|
||||
}
|
||||
}
|
||||
|
||||
$this->save();
|
||||
$this->onPublish( $member );
|
||||
|
||||
/* And update the tags perm cache */
|
||||
if ( $this instanceof \IPS\Content\Tags )
|
||||
@@ -8634,26 +8708,10 @@ abstract class _Item extends \IPS\Content
|
||||
|
||||
$comment->$date = time();
|
||||
$comment->save();
|
||||
|
||||
if ( isset( static::$databaseColumnMap['last_comment'] ) )
|
||||
{
|
||||
$lastCommentField = static::$databaseColumnMap['last_comment'];
|
||||
if ( \is_array( $lastCommentField ) )
|
||||
{
|
||||
foreach ( $lastCommentField as $column )
|
||||
{
|
||||
$this->$column = time();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->$lastCommentField = time();
|
||||
}
|
||||
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
|
||||
$this->onPublish( $member );
|
||||
|
||||
/* Mark this item as read for the author */
|
||||
$this->markRead( $this->author() );
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ trait Reactable
|
||||
{
|
||||
\IPS\Redis::i()->zIncrBy( 'trending', time() * 0.4, \get_class( $item ) .'__' . $itemId );
|
||||
}
|
||||
catch( \RedisException $e ) {}
|
||||
catch( \BadMethodCallException | \RedisException $e ) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,6 +270,7 @@ trait Reactable
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Cannot react to guest content */
|
||||
if ( !$owner->member_id )
|
||||
{
|
||||
return FALSE;
|
||||
@@ -293,6 +294,12 @@ trait Reactable
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Unacknowledged warnings */
|
||||
if ( $member->members_bitoptions['unacknowledged_warnings'] )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Still here? All good. */
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -650,4 +650,25 @@ abstract class _Review extends \IPS\Content\Comment
|
||||
/* Nope, we cannot delete */
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Respond to a review
|
||||
*
|
||||
* @param string $response
|
||||
* @return void
|
||||
*/
|
||||
public function setResponse( string $response )
|
||||
{
|
||||
$this->checkProfanityFilters( FALSE, (bool) $this->hasAuthorResponse(), $response );
|
||||
|
||||
$column = static::$databaseColumnMap['author_response'];
|
||||
$this->$column = $response;
|
||||
$this->save();
|
||||
|
||||
/* Reindex in case it was hidden by the profanity filters */
|
||||
if ( $this instanceof \IPS\Content\Searchable )
|
||||
{
|
||||
\IPS\Content\Search\Index::i()->index( $this );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -396,7 +396,7 @@ class _Query extends \IPS\Content\Search\Query
|
||||
{
|
||||
$classes = array_merge( $object->classes, $classes );
|
||||
|
||||
if ( ! $object->similarContent )
|
||||
if ( empty( $object->similarContent ) )
|
||||
{
|
||||
foreach( $object->classes as $class )
|
||||
{
|
||||
|
||||
@@ -249,6 +249,12 @@ abstract class _Index extends \IPS\Patterns\Singleton
|
||||
$isLastComment = 1;
|
||||
}
|
||||
|
||||
/* If this comment is hidden, don't actually mark as the last comment as that will cause this item to be hidden in search if we are getting only the last comment. */
|
||||
if ( $isLastComment AND $object->hidden() )
|
||||
{
|
||||
$isLastComment = 0;
|
||||
}
|
||||
|
||||
/* If we are re-indexing the first post of an item, which is triggered by a comment being added to a $itemClass::$firstCommentRequired,
|
||||
then ensure that the isLastComment flag is not added as we index the comment first, which means the index_is_last_comment is incorrectly reset to 0 for the comment itself */
|
||||
if ( $isForItem and $isLastComment )
|
||||
|
||||
@@ -588,16 +588,39 @@ class _Index extends \IPS\Content\Search\Index
|
||||
public function resetLastComment( $classes, $indexItemId, $ignoreId = NULL )
|
||||
{
|
||||
\IPS\Db::i()->update( 'core_search_index', array( 'index_is_last_comment' => 0 ), array( \IPS\Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=? AND index_is_last_comment=1', $indexItemId ) );
|
||||
|
||||
try
|
||||
{
|
||||
$latest = \IPS\Db::i()->select( 'index_object_id, index_date_updated, index_class', 'core_search_index', array( \IPS\Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=? and index_hidden=0', $indexItemId ), 'index_date_created DESC', array( 0, 1 ) )->first();
|
||||
\IPS\Db::i()->update( 'core_search_index', array( 'index_is_last_comment' => 1 ), array( 'index_class=? AND index_object_id=?', $latest['index_class'], $latest['index_object_id'] ) );
|
||||
|
||||
/* Now reset the item index with the latest comment time */
|
||||
\IPS\Db::i()->update( 'core_search_index', array( 'index_date_updated' => $latest['index_date_updated'], 'index_date_commented' => $latest['index_date_commented'] ), array( \IPS\Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=? AND index_object_id=?', $indexItemId, $indexItemId ) );
|
||||
}
|
||||
catch( \Exception $ex ) { }
|
||||
catch( \Exception )
|
||||
{
|
||||
/* Didn't find a latest? Was it because the entire thing is hidden? */
|
||||
try
|
||||
{
|
||||
$index = \IPS\Db::i()->select( 'index_id, index_item_index_id, index_hidden', 'core_search_index', array( \IPS\Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=?', $indexItemId ), 'index_date_created DESC', array( 0, 1 ) )->first();
|
||||
|
||||
/* Is the root item hidden? */
|
||||
if ( $index['index_id'] == $index['index_item_index_id'] and in_array( $index['index_hidden'], [ -1, 2 ] ) )
|
||||
{
|
||||
$latest = \IPS\Db::i()->select( 'index_object_id, index_date_updated, index_class, index_date_commented', 'core_search_index', array( \IPS\Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=? and index_hidden=?', $indexItemId, $index['index_hidden'] ), 'index_date_created DESC', array( 0, 1 ) )->first();
|
||||
}
|
||||
else
|
||||
{
|
||||
/* No, so we can't find a latest item that is hidden, so skip this */
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
catch( \Exception )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
\IPS\Db::i()->update( 'core_search_index', array( 'index_is_last_comment' => 1 ), array( 'index_class=? AND index_object_id=?', $latest['index_class'], $latest['index_object_id'] ) );
|
||||
|
||||
/* Now reset the item index with the latest comment time */
|
||||
\IPS\Db::i()->update( 'core_search_index', array( 'index_date_updated' => $latest['index_date_updated'], 'index_date_commented' => $latest['index_date_commented'] ), array( \IPS\Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=? AND index_object_id=?', $indexItemId, $indexItemId ) );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -922,7 +922,7 @@ class _Query extends \IPS\Content\Search\Query
|
||||
/* Now, if we have 501 results, then we have to assume there are more, so the join is required */
|
||||
if ( \count( $tagIds ) == 501 )
|
||||
{
|
||||
$tagWhere = array( $itemsOnlyTagSearch . 'index_item_index_id IN ( ? )', \IPS\Db::i()->select( 'index_id', 'core_search_index_tags', array( \IPS\Db::i()->in( 'index_tag', $tags ) ) ) );
|
||||
$tagWhere = array( $itemsOnlyTagSearch . 'index_item_index_id IN (' . \IPS\Db::i()->select( 'index_id', 'core_search_index_tags', array( \IPS\Db::i()->in( 'index_tag', $tags ) ) ) . ')' );
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1031,7 +1031,7 @@ class _Query extends \IPS\Content\Search\Query
|
||||
$where[] = $clause;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Return */
|
||||
return $where;
|
||||
}
|
||||
@@ -1101,7 +1101,7 @@ class _Query extends \IPS\Content\Search\Query
|
||||
search which is actually very efficient as MySQL performs a search on the FT index first to narrow down the results */
|
||||
if ( static::termIsPhrase( $term ) )
|
||||
{
|
||||
$term = str_replace( array( "'", '"' ), '', $term );
|
||||
$term = str_replace( array( "'", '"', '?' ), '', $term );
|
||||
|
||||
foreach ( static::termAsWordsArray( $term ) as $word )
|
||||
{
|
||||
@@ -1161,6 +1161,9 @@ class _Query extends \IPS\Content\Search\Query
|
||||
|
||||
/* Trailing + or -s are not allowed */
|
||||
$word = rtrim( $word, '+-' );
|
||||
|
||||
/* ? are not allowed either */
|
||||
$word = str_replace( '?', '', $word );
|
||||
|
||||
/* These rules only apply if we're not in a quoted phrase... */
|
||||
if ( !static::termIsPhrase( $word ) )
|
||||
|
||||
@@ -153,11 +153,11 @@ trait Statistics
|
||||
if ( $name )
|
||||
{
|
||||
$subQuery = \IPS\Db::i()->select( 'core_members.member_id', 'core_members', \IPS\Db::i()->like( 'core_members.name', $name ) );
|
||||
$where[] = [ $commentClass::$databaseTable . '.' . $commentClass::$databaseColumnMap['author'] . ' IN(?)', $subQuery ];
|
||||
$where[] = [ $commentClass::$databaseTable . '.' . $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['author'] . ' IN(?)', $subQuery ];
|
||||
}
|
||||
|
||||
$members = iterator_to_array( \IPS\Db::i()->select(
|
||||
$commentClass::$databaseColumnMap['author'],
|
||||
$commentClass::$databasePrefix . $commentClass::$databaseColumnMap['author'],
|
||||
$commentClass::$databaseTable,
|
||||
$where,
|
||||
NULL,
|
||||
@@ -232,7 +232,7 @@ trait Statistics
|
||||
$return = array();
|
||||
foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', $commentClass::$databaseTable, array( \IPS\Db::i()->in( $commentIdField, $postIds ) ), "FIND_IN_SET( {$commentIdField}, '" . implode( ",", $postIds ) . "' )", array( 0, $count ) ), $commentClass ) as $comment )
|
||||
{
|
||||
if ( $comment->canView() and $comment->mapped('item') == $this->$idField )
|
||||
if ( $comment->canView() and $comment->mapped('item') == $this->$idField and isset( $counts[ $comment->$commentIdField ] ) )
|
||||
{
|
||||
$return[] = array( 'comment' => $comment, 'count' => $counts[ $comment->$commentIdField ] );
|
||||
}
|
||||
@@ -402,7 +402,18 @@ trait Statistics
|
||||
protected function _getVisibleWhere()
|
||||
{
|
||||
$commentClass = static::$commentClass;
|
||||
$idField = static::$databaseColumnId;
|
||||
|
||||
$lookFor = 'IPS\cms\Records\CommentTopicSync';
|
||||
|
||||
if ( mb_substr( $commentClass, 0, mb_strlen( $lookFor ) ) === $lookFor )
|
||||
{
|
||||
$idField = 'record_topicid';
|
||||
}
|
||||
else
|
||||
{
|
||||
$idField = static::$databaseColumnId;
|
||||
}
|
||||
|
||||
$commentItemField = $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['item'];
|
||||
|
||||
$where = array();
|
||||
@@ -417,6 +428,11 @@ trait Statistics
|
||||
$where[] = array( "{$hiddenColumn} = 0" );
|
||||
}
|
||||
|
||||
if ( $commentClass::commentWhere() !== NULL )
|
||||
{
|
||||
$where[] = $commentClass::commentWhere();
|
||||
}
|
||||
|
||||
$where[] = array( $commentItemField . '=?', $this->$idField );
|
||||
|
||||
return $where;
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Views Trait
|
||||
* @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 18 Feb 2013
|
||||
*/
|
||||
|
||||
namespace IPS\Content;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Views Trait
|
||||
*/
|
||||
trait ViewUpdates
|
||||
{
|
||||
/**
|
||||
* Update View Count
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function updateViews()
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$class = \get_called_class();
|
||||
|
||||
$countUpdated = false;
|
||||
if ( \IPS\REDIS_ENABLED and \IPS\CACHE_METHOD == 'Redis' and ( \IPS\CACHE_CONFIG or \IPS\REDIS_CONFIG ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
\IPS\Redis::i()->zIncrBy( 'topic_views', 1, $class .'__' . $this->$idColumn );
|
||||
$countUpdated = true;
|
||||
}
|
||||
catch( \Exception $e ) {}
|
||||
}
|
||||
|
||||
if ( ! $countUpdated )
|
||||
{
|
||||
\IPS\Db::i()->insert( 'core_view_updates', array(
|
||||
'classname' => $class,
|
||||
'id' => $this->$idColumn
|
||||
) );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,5 +22,6 @@ if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
*
|
||||
* @note Content classes will gain special functionality by implementing this interface
|
||||
* @note Originally we were just checking for a mapped 'views' column, but decided to use an interface for consistency
|
||||
* @deprecated Use ViewUpdates trait instead.
|
||||
*/
|
||||
interface Views { }
|
||||
@@ -366,8 +366,8 @@ class _DateTime extends \DateTime
|
||||
return '%l' . ( $minutes ? ':%M' : '' ) . ( $seconds ? ':%S ' : ' ' ) . ' %p';
|
||||
}
|
||||
}
|
||||
|
||||
return '%H' . ( $minutes ? ':%M' : '' ) . ( $seconds ? ':%S ' : ' ' );
|
||||
|
||||
return '%H' . ( $minutes ? ':%M' : '' ) . ( $seconds ? ':%S ' : '' );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -494,13 +494,18 @@ class _Admin extends \IPS\Dispatcher\Standard
|
||||
{
|
||||
return $this->acpTabOrder;
|
||||
}
|
||||
|
||||
if ( isset( \IPS\Request::i()->cookie['acpTabs'] ) )
|
||||
|
||||
if ( isset( \IPS\Request::i()->cookie['acpTabs'] ) AND !\IPS\Settings::i()->acp_menu_cookie_rebuild )
|
||||
{
|
||||
$this->acpTabOrder = json_decode( \IPS\Request::i()->cookie['acpTabs'], TRUE );
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( \IPS\Settings::i()->acp_menu_cookie_rebuild )
|
||||
{
|
||||
\IPS\Settings::i()->changeValues( array( 'acp_menu_cookie_rebuild' => 0 ) );
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$this->acpTabOrder = json_decode( \IPS\Db::i()->select( 'data', 'core_acp_tab_order', array( 'id=?', \IPS\Member::loggedIn()->member_id ) )->first(), TRUE );
|
||||
|
||||
@@ -271,7 +271,7 @@ class _Api extends \IPS\Dispatcher
|
||||
/**
|
||||
* Set Language
|
||||
*
|
||||
* @return void
|
||||
* @return Lang
|
||||
*/
|
||||
public function _setLanguage()
|
||||
{
|
||||
@@ -285,6 +285,8 @@ class _Api extends \IPS\Dispatcher
|
||||
{
|
||||
$this->language = \IPS\Lang::load( \IPS\Lang::defaultLanguage() );
|
||||
}
|
||||
|
||||
return $this->language;
|
||||
}
|
||||
catch ( \OutOfRangeException $e )
|
||||
{
|
||||
|
||||
@@ -680,6 +680,7 @@ Disallow: {$path}startTopic/
|
||||
Disallow: {$path}discover/unread/
|
||||
Disallow: {$path}markallread/
|
||||
Disallow: {$path}staff/
|
||||
Disallow: {$path}cookie/
|
||||
Disallow: {$path}online/
|
||||
Disallow: {$path}discover/
|
||||
Disallow: {$path}leaderboard/
|
||||
@@ -823,7 +824,7 @@ FILE;
|
||||
}
|
||||
|
||||
/* Don't get in the way of the ModCP, registering, logging in, etc */
|
||||
$ignoreControllers = [ 'modcp', 'register', 'login', 'redirect' ];
|
||||
$ignoreControllers = [ 'modcp', 'register', 'login', 'redirect', 'cookie' ];
|
||||
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() );
|
||||
|
||||
@@ -272,7 +272,7 @@ abstract class _Standard extends \IPS\Dispatcher
|
||||
}
|
||||
|
||||
/* VLE */
|
||||
if ( isset( \IPS\Request::i()->cookie['vle_editor'] ) and \IPS\Request::i()->cookie['vle_editor'] )
|
||||
if ( \IPS\Lang::vleActive() )
|
||||
{
|
||||
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'customization/visuallanguage.css', 'core', 'admin' ) );
|
||||
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'global_customization.js', 'core', 'global' ) );
|
||||
|
||||
+31
-6
@@ -113,6 +113,36 @@ abstract class _Email
|
||||
return 'IPS\Email\Outgoing\Php';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the IPS CIC email system is used. Only affects IPS Cloud Installs
|
||||
*
|
||||
* @param string|null $type The type of email
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function usingCicEmail( ?string $type ) : bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether an email is registered as blocked by the current email platform (probably only implemented on cloud)
|
||||
*
|
||||
* @param string $email The email
|
||||
* @param string|null $type The type of email
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function emailIsBlocked( string $email, ?string $type = null ) : bool
|
||||
{
|
||||
$type = $type ?: static::TYPE_TRANSACTIONAL;
|
||||
if ( !is_subclass_of( \get_called_class(), '\IPS\Email' ) )
|
||||
{
|
||||
return static::classToUse( $type )::emailIsBlocked( $email, $type );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory
|
||||
@@ -720,12 +750,6 @@ abstract class _Email
|
||||
*/
|
||||
public function send( $to, $cc=array(), $bcc=array(), $fromEmail = NULL, $fromName = NULL, $additionalHeaders = array(), $autoSubmitted = TRUE, $updateAds = TRUE )
|
||||
{
|
||||
/* Check we have recipients */
|
||||
if ( !static::_parseRecipients( $to, TRUE ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/* Send the email */
|
||||
try
|
||||
{
|
||||
@@ -1158,6 +1182,7 @@ abstract class _Email
|
||||
*
|
||||
* @param string|array|\IPS\Member $data The member or email address, or array of members or email addresses, to send to
|
||||
* @param bool $emailOnly If TRUE, will use email only rather than names too. Set to TRUE for the "To" header
|
||||
*
|
||||
* @return string
|
||||
* @see <a href='http://www.faqs.org/rfcs/rfc2822.html'>RFC 2822</a>
|
||||
*/
|
||||
|
||||
@@ -243,7 +243,7 @@ class _Email
|
||||
}
|
||||
|
||||
/* We need to convert the text to UTF-8 if it is not already */
|
||||
if ( mb_strtolower( $charset ) != 'utf-8' )
|
||||
if ( mb_strtolower( $charset ) != 'utf-8' and ( in_array( mb_strtolower( $charset ), array_map( 'mb_strtolower', mb_list_encodings() ) ) ) )
|
||||
{
|
||||
$text = mb_convert_encoding( $text, 'UTF-8', mb_strtoupper( $charset ) );
|
||||
}
|
||||
|
||||
@@ -42,13 +42,7 @@ class _Exception extends \RuntimeException
|
||||
/* Store these for the extraLogData() method */
|
||||
$this->extraDetails = $extra;
|
||||
$this->messageKey = $message;
|
||||
|
||||
switch ( $code )
|
||||
{
|
||||
default:
|
||||
$message = \IPS\Member::loggedIn()->language()->addToStack( $message, FALSE, array( 'sprintf' => $extra ) );
|
||||
break;
|
||||
}
|
||||
$message = \IPS\Member::loggedIn()->language()->addToStack( $message, FALSE, array( 'sprintf' => $extra ) );
|
||||
|
||||
return parent::__construct( $message, $code, $previous );
|
||||
}
|
||||
|
||||
@@ -546,12 +546,11 @@ class _Amazon extends \IPS\File
|
||||
*/
|
||||
public function chunkInit( $filename, $container = '', $obscure = TRUE )
|
||||
{
|
||||
$container = trim( $container, '/' );
|
||||
|
||||
$container = $container ? trim( $container, '/' ) : $this->container;
|
||||
$this->setFilename( $filename, $obscure );
|
||||
|
||||
$path = $this->container ? "{$this->container}/{$this->filename}" : "{$this->filename}";
|
||||
|
||||
$path = $container ? "{$container}/{$this->filename}" : "{$this->filename}";
|
||||
|
||||
$r = static::makeRequest( $path, 'POST', $this->configuration, $this->configurationId, NULL, NULL, FALSE, FALSE, array( 'uploads' => '' ) );
|
||||
$uploadId = (string) $r->decodeXml()->UploadId;
|
||||
|
||||
|
||||
@@ -468,6 +468,16 @@ class _GeoLocation
|
||||
*/
|
||||
protected $map;
|
||||
|
||||
/**
|
||||
* Get Requester Location
|
||||
*
|
||||
* @return \IPS\GeoLocation
|
||||
*/
|
||||
public static function getRequesterLocation(): \IPS\GeoLocation
|
||||
{
|
||||
return static::getByIp( \IPS\Request::i()->ipAddress() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get by IP address
|
||||
*
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
|
||||
namespace IPS\Helpers\Chart;
|
||||
|
||||
use IPS\Member;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
@@ -107,6 +109,16 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
|
||||
*/
|
||||
public $showIntervals = TRUE;
|
||||
|
||||
/**
|
||||
* @brief Allow user to adjust date range
|
||||
*/
|
||||
public $showDateRange = TRUE;
|
||||
|
||||
/**
|
||||
* @brief Show save button
|
||||
*/
|
||||
public $showSave = TRUE;
|
||||
|
||||
/**
|
||||
* @brief If a warning about timezones needs to be shown
|
||||
*/
|
||||
@@ -367,48 +379,80 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
|
||||
{
|
||||
try
|
||||
{
|
||||
/* If we have filters, we can save them */
|
||||
if( \count( $this->availableFilters ) > 0 or $this->customFiltersForm )
|
||||
{
|
||||
/* Generate a form so we can save our filters as a new saved chart */
|
||||
$this->form = new \IPS\Helpers\Form;
|
||||
$this->form->class = 'ipsForm_vertical';
|
||||
/* Generate a form so we can save our filters as a new saved chart */
|
||||
$this->form = new \IPS\Helpers\Form;
|
||||
$this->form->class = 'ipsForm_vertical';
|
||||
|
||||
if ( $this->customFiltersForm )
|
||||
if ( $this->customFiltersForm )
|
||||
{
|
||||
if ( $customValues = $this->getCustomFiltersForm()->values( TRUE ) )
|
||||
{
|
||||
if ( $customValues = $this->getCustomFiltersForm()->values( TRUE ) )
|
||||
foreach( $customValues as $key => $value )
|
||||
{
|
||||
foreach( $customValues as $key => $value )
|
||||
{
|
||||
$this->form->hiddenValues['customform_' . $key ] = $value;
|
||||
}
|
||||
$this->form->hiddenValues['customform_' . $key ] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* If we have a sub selector from a node form, add on the identifier and pass off to the form to show the ajax */
|
||||
if ( \IPS\Request::i()->_nodeSelectName )
|
||||
/* If we have a sub selector from a node form, add on the identifier and pass off to the form to show the ajax */
|
||||
if ( \IPS\Request::i()->_nodeSelectName )
|
||||
{
|
||||
\IPS\Request::i()->_nodeSelectName = $this->identifier . \IPS\Request::i()->_nodeSelectName;
|
||||
return (string) $this->getCustomFiltersForm();
|
||||
}
|
||||
|
||||
/* We have an existing chart ID */
|
||||
if( isset( \IPS\Request::i()->chartId ) AND \IPS\Request::i()->chartId != '_default' )
|
||||
{
|
||||
$title = '';
|
||||
|
||||
foreach( $this->loadAvailableChartTabs() as $chart )
|
||||
{
|
||||
\IPS\Request::i()->_nodeSelectName = $this->identifier . \IPS\Request::i()->_nodeSelectName;
|
||||
return (string) $this->getCustomFiltersForm();
|
||||
if( $chart['chart_id'] == \IPS\Request::i()->chartId )
|
||||
{
|
||||
$title = $chart['chart_title'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* We have an existing chart ID */
|
||||
if( isset( \IPS\Request::i()->chartId ) AND \IPS\Request::i()->chartId != '_default' )
|
||||
$custom = array();
|
||||
$chartFilters = ( isset( \IPS\Request::i()->chartFilters ) and \IPS\Request::i()->chartFilters ) ? \IPS\Request::i()->chartFilters : array();
|
||||
$this->timescale = $chart['chart_timescale'] ?? 'monthly';
|
||||
|
||||
foreach( \IPS\Request::i() as $key => $value )
|
||||
{
|
||||
$title = '';
|
||||
|
||||
foreach( $this->loadAvailableChartTabs() as $chart )
|
||||
if ( \mb_substr( $key, 0, 11 ) === 'customform_' )
|
||||
{
|
||||
if( $chart['chart_id'] == \IPS\Request::i()->chartId )
|
||||
{
|
||||
$title = $chart['chart_title'];
|
||||
break;
|
||||
}
|
||||
$custom[ preg_replace( '#' . $this->identifier . '#', '', $key ) ] = $value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$this->form->add( new \IPS\Helpers\Form\Text( 'custom_chart_title', $title, TRUE ) );
|
||||
|
||||
if( $values = $this->form->values() )
|
||||
{
|
||||
\IPS\Db::i()->update( 'core_saved_charts', array( 'chart_title' => $values['custom_chart_title'] ), array( 'chart_id=? AND chart_member=?', \IPS\Request::i()->chartId, \IPS\Member::loggedIn()->member_id ) );
|
||||
|
||||
/* And then return the output we need */
|
||||
\IPS\Output::i()->json( array(
|
||||
'title' => $values['custom_chart_title']
|
||||
) );
|
||||
}
|
||||
|
||||
/* And we want to save our filter updates */
|
||||
if( isset( \IPS\Request::i()->saveFilters ) )
|
||||
{
|
||||
\IPS\Db::i()->update( 'core_saved_charts', array( 'chart_configuration' => json_encode( array_merge( $chartFilters, $custom ) ), 'chart_timescale' => \IPS\Request::i()->timescale ?? $this->timescale ?? 'monthly' ), array( 'chart_id=? AND chart_member=?', \IPS\Request::i()->chartId, \IPS\Member::loggedIn()->member_id ) );
|
||||
}
|
||||
}
|
||||
/* We are not viewing a saved chart */
|
||||
else
|
||||
{
|
||||
$this->form->add( new \IPS\Helpers\Form\Text( 'custom_chart_title', NULL, TRUE ) );
|
||||
|
||||
if( $values = $this->form->values() )
|
||||
{
|
||||
$custom = array();
|
||||
$chartFilters = ( isset( \IPS\Request::i()->chartFilters ) and \IPS\Request::i()->chartFilters ) ? \IPS\Request::i()->chartFilters : array();
|
||||
|
||||
foreach( \IPS\Request::i() as $key => $value )
|
||||
{
|
||||
if ( \mb_substr( $key, 0, 11 ) === 'customform_' )
|
||||
@@ -417,74 +461,40 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
|
||||
}
|
||||
}
|
||||
|
||||
$this->form->add( new \IPS\Helpers\Form\Text( 'custom_chart_title', $title, TRUE ) );
|
||||
$chartFilters = ( isset( \IPS\Request::i()->chartFilters ) and \IPS\Request::i()->chartFilters ) ? \IPS\Request::i()->chartFilters : array();
|
||||
|
||||
/* Store the new chart */
|
||||
$id = \IPS\Db::i()->insert( 'core_saved_charts', array(
|
||||
'chart_member' => \IPS\Member::loggedIn()->member_id,
|
||||
'chart_controller' => \IPS\Request::i()->app . '_' . \IPS\Request::i()->module . '_' . \IPS\Request::i()->controller . ( \IPS\Request::i()->tab ? '_' . \IPS\Request::i()->tab : '' ),
|
||||
'chart_configuration' => json_encode( array_merge( $chartFilters, $custom ) ),
|
||||
'chart_timescale' => \IPS\Request::i()->timescale ?? $this->timescale ?? 'monthly',
|
||||
'chart_title' => $values['custom_chart_title'],
|
||||
) );
|
||||
|
||||
if( $values = $this->form->values() )
|
||||
{
|
||||
\IPS\Db::i()->update( 'core_saved_charts', array( 'chart_title' => $values['custom_chart_title'] ), array( 'chart_id=? AND chart_member=?', \IPS\Request::i()->chartId, \IPS\Member::loggedIn()->member_id ) );
|
||||
/* Set some input parameters */
|
||||
$this->url = $this->url->setQueryString( 'chartId', $id );
|
||||
\IPS\Request::i()->chartId = $id;
|
||||
\IPS\Request::i()->filters = array( $this->identifier => \IPS\Request::i()->chartFilters );
|
||||
|
||||
/* And then return the output we need */
|
||||
\IPS\Output::i()->json( array(
|
||||
'title' => $values['custom_chart_title']
|
||||
) );
|
||||
}
|
||||
|
||||
/* And we want to save our filter updates */
|
||||
if( isset( \IPS\Request::i()->saveFilters ) )
|
||||
{
|
||||
\IPS\Db::i()->update( 'core_saved_charts', array( 'chart_configuration' => json_encode( array_merge( $chartFilters, $custom ) ) ), array( 'chart_id=? AND chart_member=?', \IPS\Request::i()->chartId, \IPS\Member::loggedIn()->member_id ) );
|
||||
}
|
||||
}
|
||||
/* We are not viewing a saved chart */
|
||||
else
|
||||
{
|
||||
$this->form->add( new \IPS\Helpers\Form\Text( 'custom_chart_title', NULL, TRUE ) );
|
||||
$this->currentFilters = \IPS\Request::i()->filters;
|
||||
|
||||
if( $values = $this->form->values() )
|
||||
if ( isset( \IPS\Request::i()->filters[ $this->identifier ] ) )
|
||||
{
|
||||
$custom = array();
|
||||
foreach( \IPS\Request::i() as $key => $value )
|
||||
{
|
||||
if ( \mb_substr( $key, 0, 11 ) === 'customform_' )
|
||||
{
|
||||
$custom[ preg_replace( '#' . $this->identifier . '#', '', $key ) ] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$chartFilters = ( isset( \IPS\Request::i()->chartFilters ) and \IPS\Request::i()->chartFilters ) ? \IPS\Request::i()->chartFilters : array();
|
||||
|
||||
/* Store the new chart */
|
||||
$id = \IPS\Db::i()->insert( 'core_saved_charts', array(
|
||||
'chart_member' => \IPS\Member::loggedIn()->member_id,
|
||||
'chart_controller' => \IPS\Request::i()->app . '_' . \IPS\Request::i()->module . '_' . \IPS\Request::i()->controller . ( \IPS\Request::i()->tab ? '_' . \IPS\Request::i()->tab : '' ),
|
||||
'chart_configuration' => json_encode( array_merge( $chartFilters, $custom ) ),
|
||||
'chart_title' => $values['custom_chart_title'],
|
||||
) );
|
||||
|
||||
/* Set some input parameters */
|
||||
$this->url = $this->url->setQueryString( 'chartId', $id );
|
||||
\IPS\Request::i()->chartId = $id;
|
||||
\IPS\Request::i()->filters = array( $this->identifier => \IPS\Request::i()->chartFilters );
|
||||
|
||||
$this->currentFilters = \IPS\Request::i()->filters;
|
||||
|
||||
if ( isset( \IPS\Request::i()->filters[ $this->identifier ] ) )
|
||||
{
|
||||
$this->url = $this->url->setQueryString( 'filters', array( $this->identifier => $this->currentFilters ) );
|
||||
}
|
||||
|
||||
/* Reset form, since template looks for it and it should not be set for a saved chart */
|
||||
$this->form = new \IPS\Helpers\Form;
|
||||
$this->form->class = 'ipsForm_vertical';
|
||||
$this->form->add( new \IPS\Helpers\Form\Text( 'custom_chart_title', $values['custom_chart_title'], TRUE ) );
|
||||
|
||||
/* And then return the output we need */
|
||||
\IPS\Output::i()->json( array(
|
||||
'tabHref' => $this->url->stripQueryString( 'filters' ),
|
||||
'chartId' => $id,
|
||||
'tabId' => md5( $this->url->acpQueryString() ),
|
||||
) );
|
||||
$this->url = $this->url->setQueryString( 'filters', array( $this->identifier => $this->currentFilters ) );
|
||||
}
|
||||
|
||||
/* Reset form, since template looks for it and it should not be set for a saved chart */
|
||||
$this->form = new \IPS\Helpers\Form;
|
||||
$this->form->class = 'ipsForm_vertical';
|
||||
$this->form->add( new \IPS\Helpers\Form\Text( 'custom_chart_title', $values['custom_chart_title'], TRUE ) );
|
||||
|
||||
/* And then return the output we need */
|
||||
\IPS\Output::i()->json( array(
|
||||
'tabHref' => $this->url->stripQueryString( 'filters' ),
|
||||
'chartId' => $id,
|
||||
'tabId' => md5( $this->url->acpQueryString() ),
|
||||
) );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -507,8 +517,14 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
|
||||
else
|
||||
{
|
||||
$chartOutput = \IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->dynamicChart( $this, $output );
|
||||
|
||||
/* If we're not showing filter tabs, just return here. */
|
||||
if ( !$this->showFilterTabs )
|
||||
{
|
||||
return $chartOutput;
|
||||
}
|
||||
|
||||
if( ( \count( $this->availableFilters ) > 0 or $this->customFiltersForm ) AND ( !\IPS\Request::i()->isAjax() OR ( \IPS\Request::i()->tab AND !\IPS\Request::i()->chartId ) ) )
|
||||
if( !\IPS\Request::i()->isAjax() OR ( \IPS\Request::i()->tab AND !\IPS\Request::i()->chartId ) )
|
||||
{
|
||||
return \IPS\Theme::i()->getTemplate( 'global', 'core' )->tabs( $this->getChartTabs(), ( isset( \IPS\Request::i()->chartId ) ) ? \IPS\Request::i()->chartId : NULL, $chartOutput, $this->url, 'chartId', 'ipsTabs_small ipsTabs_contained' );
|
||||
}
|
||||
@@ -527,11 +543,21 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
|
||||
\IPS\IPS::exceptionHandler( $e );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Show filter tabs
|
||||
*/
|
||||
public $showFilterTabs = TRUE;
|
||||
|
||||
/**
|
||||
* @brief Cached tab data
|
||||
*/
|
||||
protected $availableChartTabs = NULL;
|
||||
|
||||
/**
|
||||
* @Brief Extension
|
||||
*/
|
||||
public $extension = NULL;
|
||||
|
||||
/**
|
||||
* Retrieve tabs based on saved charts
|
||||
@@ -559,11 +585,31 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
|
||||
{
|
||||
if( $this->availableChartTabs === NULL )
|
||||
{
|
||||
$this->availableChartTabs = iterator_to_array( \IPS\Db::i()->select( '*', 'core_saved_charts', array( 'chart_member=? AND chart_controller=?', \IPS\Member::loggedIn()->member_id, \IPS\Request::i()->app . '_' . \IPS\Request::i()->module . '_' . \IPS\Request::i()->controller . ( \IPS\Request::i()->tab ? '_' . \IPS\Request::i()->tab : '' ) ) ) );
|
||||
if ( $this->extension !== NULL )
|
||||
{
|
||||
$this->availableChartTabs = iterator_to_array( \IPS\Db::i()->select( '*', 'core_saved_charts', array( 'chart_member=? AND chart_controller=?', \IPS\Member::loggedIn()->member_id, $this->extension->controller ) ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->availableChartTabs = iterator_to_array( \IPS\Db::i()->select( '*', 'core_saved_charts', array( 'chart_member=? AND chart_controller=?', \IPS\Member::loggedIn()->member_id, \IPS\Request::i()->app . '_' . \IPS\Request::i()->module . '_' . \IPS\Request::i()->controller . ( \IPS\Request::i()->tab ? '_' . \IPS\Request::i()->tab : '' ) ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
return $this->availableChartTabs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set extension
|
||||
*
|
||||
* @param \IPS\core\Statistics\Chart $ext Extension
|
||||
* @return void
|
||||
*/
|
||||
public function setExtension( \IPS\core\Statistics\Chart $ext )
|
||||
{
|
||||
$this->extension = $ext;
|
||||
|
||||
$this->availableChartTabs = NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flip URL Filter
|
||||
@@ -685,10 +731,22 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
|
||||
{
|
||||
$rawHeaders = $this->headers;
|
||||
}
|
||||
|
||||
|
||||
/* Ok this is a very horrible hack to get the language hashes converted into the real names and in a format
|
||||
that we can then reparse later. */
|
||||
$uglyHack = '';
|
||||
foreach( $rawHeaders AS $data )
|
||||
{
|
||||
$headers[] = $data['label'];
|
||||
$uglyHack .= str_replace( "\n", " ", $data['label'] ) . "\n";
|
||||
}
|
||||
|
||||
/* This really is awful */
|
||||
Member::loggedIn()->language()->parseOutputForDisplay( $uglyHack );
|
||||
|
||||
foreach( explode( "\n", trim( $uglyHack ) ) as $label )
|
||||
{
|
||||
/* Now we have the true string, eg: News, Now! instead of a language hash, eg: 77e90423a7642378a1590420cd66a465 so fputcsv can escape it correctly */
|
||||
$headers[] = $label;
|
||||
}
|
||||
fputcsv( $fh, $headers );
|
||||
|
||||
|
||||
@@ -160,7 +160,7 @@ class _Editor extends FormAbstract
|
||||
|
||||
/* Don't minimize if we have a value */
|
||||
$name = isset( $options['editorId'] ) ? $options['editorId'] : $name;
|
||||
if ( ( !isset( $options['minimizeWithContent'] ) or !$options['minimizeWithContent'] ) and ( $defaultValue or \IPS\Request::i()->$name or ( isset( \IPS\Request::i()->cookie['vle_editor'] ) and \IPS\Request::i()->cookie['vle_editor'] ) ) )
|
||||
if ( ( !isset( $options['minimizeWithContent'] ) or !$options['minimizeWithContent'] ) and ( $defaultValue or \IPS\Request::i()->$name or ( \IPS\Lang::vleActive() ) ) )
|
||||
{
|
||||
$options['minimize'] = NULL;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ class _Email extends Text
|
||||
public $childDefaultOptions = array(
|
||||
'accountEmail' => FALSE,
|
||||
'htmlAutocomplete' => "email",
|
||||
'bypassProfanity' => \IPS\Helpers\Form\Text::BYPASS_PROFANITY_ALL
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -65,6 +66,12 @@ class _Email extends Text
|
||||
/* Check Banned and Allowed Emails only if the data are not coming from an administrator */
|
||||
if ( !\IPS\Member::loggedIn()->isAdmin() )
|
||||
{
|
||||
/* Check it's not known to be undeliverable */
|
||||
if ( \IPS\Email::emailIsBlocked( $value ) )
|
||||
{
|
||||
throw new \DomainException( 'member_email_blocked_info' );
|
||||
}
|
||||
|
||||
/* Check it's not a banned address */
|
||||
foreach ( \IPS\Db::i()->select( 'ban_content', 'core_banfilters', array( "ban_type=?", 'email' ) ) as $bannedEmail )
|
||||
{
|
||||
|
||||
@@ -51,22 +51,27 @@ class _Member extends Text
|
||||
public function __construct( $name, $defaultValue=NULL, $required=FALSE, $options=array(), $customValidationCode=NULL, $prefix=NULL, $suffix=NULL, $id=NULL )
|
||||
{
|
||||
$this->defaultOptions['autocomplete'] = array(
|
||||
'source' => 'app=core&module=system&controller=ajax&do=findMember',
|
||||
'resultItemTemplate' => 'core.autocomplete.memberItem',
|
||||
'commaTrigger' => false,
|
||||
'unique' => true,
|
||||
'minAjaxLength' => 3,
|
||||
'disallowedCharacters' => array(),
|
||||
'lang' => 'mem_optional',
|
||||
'source' => 'app=core&module=system&controller=ajax&do=findMember',
|
||||
'resultItemTemplate' => 'core.autocomplete.memberItem',
|
||||
'commaTrigger' => false,
|
||||
'unique' => true,
|
||||
'minAjaxLength' => 3,
|
||||
'disallowedCharacters' => array(),
|
||||
'lang' => 'mem_optional',
|
||||
);
|
||||
if( \count( $options ) and array_key_exists( 'multiple', $options ) and $options['multiple'] > 0 )
|
||||
if (\count($options) and array_key_exists('multiple', $options) and $options['multiple'] > 0)
|
||||
{
|
||||
$this->defaultOptions['autocomplete']['maxItems'] = $options['multiple'];
|
||||
}
|
||||
elseif ( !array_key_exists( 'multiple', $options ) )
|
||||
} elseif (!array_key_exists('multiple', $options))
|
||||
{
|
||||
$this->defaultOptions['autocomplete']['maxItems'] = $this->childDefaultOptions['multiple'];
|
||||
}
|
||||
|
||||
/* Explicitly merge autocomplete options */
|
||||
if( array_key_exists('autocomplete', $options ) )
|
||||
{
|
||||
$options['autocomplete'] = array_merge( $options['autocomplete'], $this->defaultOptions['autocomplete'] );
|
||||
}
|
||||
|
||||
parent::__construct( $name, $defaultValue, $required, $options, $customValidationCode, $prefix, $suffix, $id );
|
||||
}
|
||||
|
||||
@@ -43,7 +43,8 @@ class _Text extends TextArea
|
||||
'resultItemTemplate'=> 'core.foo.bar', // Can be used to specify a custom JavaScript template to use for the result
|
||||
'minAjaxLength' => 3, // Minimum length of value before sending AJAX lookup call
|
||||
'disallowedCharacters' => array(), // An array of disallowed characters (default < > ' ")
|
||||
'minimized' => TRUE // Whether the autocomplete shows a 'choose' link to activate. Existing values or required = true will always override this.
|
||||
'minimized' => TRUE // Whether the autocomplete shows a 'choose' link to activate. Existing values or required = true will always override this.
|
||||
'alphabetical' => FALSE, // Force values to be sorted alphabetically.
|
||||
),
|
||||
'placeholder' => 'e.g. ...', // A placeholder (NB: Will only work on compatible browsers)
|
||||
'regex' => '/[A-Z]+/i', // RegEx of acceptable value
|
||||
@@ -202,6 +203,11 @@ class _Text extends TextArea
|
||||
{
|
||||
$return = array_filter( array_map( 'trim', explode( "\n", $return ) ) );
|
||||
}
|
||||
|
||||
if ( \is_array( $return ) AND isset( $this->options['autocomplete']['alphabetical'] ) AND $this->options['autocomplete']['alphabetical'] )
|
||||
{
|
||||
natcasesort( $return );
|
||||
}
|
||||
}
|
||||
|
||||
/* Remove all invisible characters if this is a username */
|
||||
|
||||
@@ -244,7 +244,8 @@ class _Upload extends FormAbstract
|
||||
|
||||
/* If there is an error, an exception will be thrown and will be caught below like normal */
|
||||
\IPS\File::validateUpload( $fileArray, $this->options['allowedFileTypes'], $this->options['maxFileSize'] );
|
||||
|
||||
|
||||
$exif = NULL;
|
||||
if ( $fileObj->isImage() )
|
||||
{
|
||||
try
|
||||
@@ -254,6 +255,7 @@ class _Upload extends FormAbstract
|
||||
if( $image::exifSupported() )
|
||||
{
|
||||
$image->setExifData( $fileObj->contents() );
|
||||
$exif = $image->parseExif();
|
||||
}
|
||||
|
||||
$hasBeenResized = $image->resizeToMax( $this->options['image']['maxWidth'] ?? NULL, $this->options['image']['maxHeight'] ?? NULL );
|
||||
@@ -266,16 +268,17 @@ class _Upload extends FormAbstract
|
||||
}
|
||||
catch ( \Exception $e ) { }
|
||||
}
|
||||
|
||||
|
||||
$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']
|
||||
'storage_extension' => $this->options['storageExtension'],
|
||||
'exif' => $exif ? json_encode( $exif ) : NULL
|
||||
) );
|
||||
|
||||
|
||||
if ( $this->options['callback'] )
|
||||
{
|
||||
$callbackFunction = $this->options['callback'];
|
||||
@@ -466,7 +469,7 @@ class _Upload extends FormAbstract
|
||||
|
||||
/* The html() method is called more than once, however $this->value is wiped out so if it was an attachments array all of the array properties are lost */
|
||||
$templateFunction = $this->template;
|
||||
$this->builtHtml = $templateFunction( $this->name, $this->value, $this->options['minimize'], $maxFileSize, $this->options['maxFiles'], $this->maxChunkSize, $this->options['totalMaxSize'], $this->options['allowedFileTypes'], $uploadKey, $this->options['multiple'], $this->options['postKey'], $this->options['temporary'] or ( isset( \IPS\Request::i()->cookie['vle_editor'] ) and \IPS\Request::i()->cookie['vle_editor'] ), $this->options['template'], $existing, $this->options['default'], $this->options['supportsDelete'], $maxImageDims, $this->options['allowStockPhotos'] );
|
||||
$this->builtHtml = $templateFunction( $this->name, $this->value, $this->options['minimize'], $maxFileSize, $this->options['maxFiles'], $this->maxChunkSize, $this->options['totalMaxSize'], $this->options['allowedFileTypes'], $uploadKey, $this->options['multiple'], $this->options['postKey'], $this->options['temporary'] or ( \IPS\Lang::vleActive() ), $this->options['template'], $existing, $this->options['default'], $this->options['supportsDelete'], $maxImageDims, $this->options['allowStockPhotos'] );
|
||||
return $this->builtHtml;
|
||||
}
|
||||
|
||||
@@ -721,8 +724,9 @@ class _Upload extends FormAbstract
|
||||
{
|
||||
$options = $this->options;
|
||||
$canBeModerated = $options['canBeModerated'];
|
||||
$exifData = array();
|
||||
|
||||
$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 )
|
||||
$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, &$exifData )
|
||||
{
|
||||
$ext = mb_strtolower( mb_substr( $filename, mb_strrpos( $filename, '.' ) + 1 ) );
|
||||
|
||||
@@ -740,6 +744,7 @@ class _Upload extends FormAbstract
|
||||
/* If type JPG, image handler may strip meta data on image output */
|
||||
if( $hasBeenResized OR $image->hasBeenRotated OR $image->type == 'jpeg' )
|
||||
{
|
||||
$exifData[ $i ] = $image->parseExif();
|
||||
$contents = (string) $image;
|
||||
}
|
||||
}
|
||||
@@ -748,14 +753,14 @@ class _Upload extends FormAbstract
|
||||
|
||||
return $contents;
|
||||
}, $this->options['storageContainer'], $this->options['obscure'] );
|
||||
|
||||
|
||||
foreach ( $fileObjects as $i => $fileObj )
|
||||
{
|
||||
$exif = NULL;
|
||||
|
||||
if( \IPS\Image::exifSupported() and $fileObj->isImage() and $fileObj->exifData )
|
||||
if( isset( $exifData[ $i ] ) AND $fileExifData = $exifData[ $i ] OR ( \IPS\Image::exifSupported() and $fileObj->isImage() and $fileExifData = $fileObj->exifData ) )
|
||||
{
|
||||
$exif = json_encode( $fileObj->exifData );
|
||||
$exif = json_encode( $fileExifData );
|
||||
}
|
||||
|
||||
$data = $this->populateTempData( array(
|
||||
|
||||
@@ -452,8 +452,8 @@ class _Content extends Table
|
||||
{
|
||||
$itemIds[] = $item->$idField;
|
||||
}
|
||||
|
||||
foreach( \IPS\Db::i()->select( '*', 'core_reputation_index', array( 'rep_class=? AND ' . \IPS\Db::i()->in( 'item_id', $itemIds ), $class::$commentClass ) ) as $react )
|
||||
|
||||
foreach( \IPS\Db::i()->select( '*', 'core_reputation_index', array( 'rep_class=? AND ' . \IPS\Db::i()->in( 'item_id', $itemIds ), $class::$commentClass ), NULL, 1000 ) as $react )
|
||||
{
|
||||
$reactions[ $react['item_id'] ][] = $react;
|
||||
}
|
||||
@@ -1078,7 +1078,7 @@ class _Content extends Table
|
||||
$class = $this->class;
|
||||
|
||||
/* ID and Title columns should be ascending */
|
||||
if( $column != $this->defaultSortBy AND ( $column == $class::$databaseColumnId OR $column == $class::$databaseColumnMap[ 'title' ] ) )
|
||||
if( isset( $class::$databaseColumnMap[ 'title' ] ) AND $column != $this->defaultSortBy AND ( $column == $class::$databaseColumnId OR $column == $class::$databaseColumnMap[ 'title' ] ) )
|
||||
{
|
||||
return 'asc';
|
||||
}
|
||||
|
||||
@@ -316,7 +316,7 @@ class _Db extends Table
|
||||
}
|
||||
|
||||
/* Count results (for pagination) */
|
||||
$count = $this->db->select( 'count(*)', $this->table, $where );
|
||||
$count = $this->db->select( 'count(*)', $this->table, $where, NULL, NULL, $this->groupBy );
|
||||
if ( \count( $this->joins ) )
|
||||
{
|
||||
foreach( $this->joins as $join )
|
||||
@@ -324,7 +324,9 @@ class _Db extends Table
|
||||
$count->join( $join['from'], ( isset( $join['where'] ) ? $join['where'] : null ), ( isset( $join['type'] ) ) ? $join['type'] : 'LEFT' );
|
||||
}
|
||||
}
|
||||
$count = $count->first();
|
||||
|
||||
$count = $this->groupBy ? $count->count() : $count->first();
|
||||
|
||||
$selectPrefix = ( $this->groupBy ) ? '' : $this->table . '.*, ';
|
||||
|
||||
/* Now get column headers */
|
||||
|
||||
@@ -160,6 +160,10 @@ class _Curl
|
||||
public function __destruct()
|
||||
{
|
||||
curl_close( $this->curl );
|
||||
if( static::$_multiHandle instanceof \CurlMultiHandle )
|
||||
{
|
||||
curl_multi_close( static::$_multiHandle );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -378,6 +382,7 @@ class _Curl
|
||||
/**
|
||||
* Execute the request
|
||||
*
|
||||
* @todo Remove if multi handle works out long term.
|
||||
* @return \IPS\Http\Response
|
||||
* @throws \IPS\Http\Request\CurlException
|
||||
*/
|
||||
@@ -407,6 +412,75 @@ class _Curl
|
||||
/* Return it */
|
||||
return new \IPS\Http\Response( $output );
|
||||
}
|
||||
|
||||
/**
|
||||
* @breif Store the cURL Multi Handle
|
||||
*/
|
||||
protected static $_multiHandle;
|
||||
|
||||
/**
|
||||
* Execute the request via cURL Multi - We use this to cache and share connections between requests
|
||||
*
|
||||
* @return \IPS\Http\Response
|
||||
* @throws \IPS\Http\Request\CurlException
|
||||
*/
|
||||
protected function _executeMh(): \IPS\Http\Response
|
||||
{
|
||||
/* Init multi handle if needed */
|
||||
if( empty( static::$_multiHandle ) )
|
||||
{
|
||||
static::$_multiHandle = curl_multi_init();
|
||||
}
|
||||
|
||||
/* Store handle for this request */
|
||||
curl_multi_add_handle( static::$_multiHandle, $this->curl );
|
||||
|
||||
/* Execute request and wait for response */
|
||||
$active = NULL;
|
||||
do
|
||||
{
|
||||
$ret = curl_multi_exec( static::$_multiHandle, $active );
|
||||
}
|
||||
while( $ret == CURLM_CALL_MULTI_PERFORM );
|
||||
|
||||
while( $active && $ret == CURLM_OK )
|
||||
{
|
||||
if( curl_multi_select( static::$_multiHandle ) != -1 )
|
||||
{
|
||||
do
|
||||
{
|
||||
$mrc = curl_multi_exec( static::$_multiHandle, $active );
|
||||
}
|
||||
while( $mrc == CURLM_CALL_MULTI_PERFORM );
|
||||
}
|
||||
}
|
||||
|
||||
$output = curl_multi_getcontent( $this->curl );
|
||||
|
||||
/* Remove handles we no longer need */
|
||||
curl_multi_remove_handle( static::$_multiHandle, $this->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" . implode( "\n", $this->headersForLog ) . "\n\n" . var_export( $this->dataForLog, TRUE ) . "\n\n------------------------------------\nRESPONSE\n------------------------------------\n\n" . $output, 'request' );
|
||||
}
|
||||
|
||||
/* Errors? */
|
||||
if ( $output === FALSE )
|
||||
{
|
||||
throw new CurlException( $this->url . "\n" . curl_error( $this->curl ), curl_errno( $this->curl ) );
|
||||
}
|
||||
|
||||
/* If this is FTP we need to fudge the headers a little */
|
||||
if( isset( $this->url->data['scheme'] ) and $this->url->data['scheme'] == 'ftp' )
|
||||
{
|
||||
$output = "HTTP/1.1 200 OK\nFTP: True\r\n\r\n" . $output;
|
||||
}
|
||||
|
||||
/* Return it */
|
||||
return new \IPS\Http\Response( $output );
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the request and follow redirects id necessary
|
||||
@@ -419,7 +493,7 @@ class _Curl
|
||||
protected function _executeAndFollowRedirects( $method, $params=NULL )
|
||||
{
|
||||
/* Execute */
|
||||
$response = $this->_execute();
|
||||
$response = $this->_executeMh();
|
||||
|
||||
/* Either return it or follow it */
|
||||
if ( $this->followRedirects and \in_array( $response->httpResponseCode, array( 301, 302, 303, 307, 308 ) ) )
|
||||
|
||||
+1
-1
@@ -638,7 +638,7 @@ class _Url
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return (string) $this->url;
|
||||
return (string) $this->url;
|
||||
}
|
||||
|
||||
/* !Encoding and Decoding */
|
||||
|
||||
@@ -91,26 +91,9 @@ class _Useragent
|
||||
*/
|
||||
public function parseUserAgent()
|
||||
{
|
||||
/* Is it ours? */
|
||||
if ( preg_match( '/^InvisionCommunityApp\/(\d+\.\d+\.\d+) \((.+?)\) (.+?) \((.+?)\)$/', $this->useragent, $matches ) ) // Matches: 1 = App version, 2 = App name (differentiates multicommunity vs white-label) 3 = "Android" or "iOS/X.Y.Z" 4 = Semi-human-friendly name (e.g. "iPhone11,6" is iPhone XS Max)
|
||||
{
|
||||
if ( $matches[3] === 'Android' )
|
||||
{
|
||||
$this->platform = $matches[4];
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->platform = preg_replace( '/^(.+?)\d+,\d+$/', '$1', $matches[4] );
|
||||
}
|
||||
|
||||
$this->browser = 'InvisionCommunityApp';
|
||||
$this->browserVersion = $matches[1];
|
||||
return;
|
||||
}
|
||||
|
||||
/* Set basic data */
|
||||
require_once( \IPS\ROOT_PATH . '/system/3rd_party/PhpUserAgent/UserAgentParser.php' );
|
||||
$data = parse_user_agent( $this->useragent );
|
||||
$data = \donatj\UserAgent\parse_user_agent( $this->useragent );
|
||||
$this->platform = $data['platform'];
|
||||
$this->browser = $data['browser'];
|
||||
$this->browserVersion = $data['version'];
|
||||
|
||||
+63
-7
@@ -1439,7 +1439,7 @@ class _Lang extends \IPS\Node\Model
|
||||
|
||||
$buttons = array_merge( $buttons, parent::getButtons( $url, $subnode ) );
|
||||
|
||||
if ( \IPS\Member::loggedIn()->hasAcpRestriction( 'core', 'languages', 'lang_words' ) AND !$this->marketplace_id )
|
||||
if ( \IPS\Member::loggedIn()->hasAcpRestriction( 'core', 'languages', 'lang_words' ) )
|
||||
{
|
||||
$buttons['upload'] = array(
|
||||
'icon' => 'upload',
|
||||
@@ -1751,9 +1751,47 @@ class _Lang extends \IPS\Node\Model
|
||||
*/
|
||||
public function stripVLETags( $output )
|
||||
{
|
||||
/* We accidentally called stripVLE for each response, even if VLE was disabled, which had a side effect of casting everything to a string which we really want and need */
|
||||
if ( ! static::vleActive() )
|
||||
{
|
||||
if( !\is_array( $output ) )
|
||||
{
|
||||
return (string) $output;
|
||||
}
|
||||
|
||||
$replacement = array();
|
||||
foreach ( $output as $key => $value )
|
||||
{
|
||||
$replacement[ $key ] = $this->stripVLETags( $value );
|
||||
}
|
||||
|
||||
return $replacement;
|
||||
}
|
||||
|
||||
if( !\is_array( $output ) )
|
||||
{
|
||||
return preg_replace( "/#VLE#.+?#\!#\[(.+?)\]#\!##/", "$1", $output );
|
||||
preg_match_all( '/#VLE#(.+?)#!#/', (string) $output, $matches, PREG_SET_ORDER );
|
||||
|
||||
foreach( $matches as $match )
|
||||
{
|
||||
$replace = $match[1]; /* Possibly better than nothing */
|
||||
|
||||
if ( isset( $this->words[$match[1]] ) )
|
||||
{
|
||||
$replace = $this->words[$match[1]];
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
$replace = $this->get( $match[1] );
|
||||
}
|
||||
catch( \OutOfRangeException $e ) {}
|
||||
}
|
||||
$output = str_replace( $match[0], $replace, $output );
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
$replacement = array();
|
||||
@@ -2033,12 +2071,9 @@ class _Lang extends \IPS\Node\Model
|
||||
}
|
||||
|
||||
/* Add VLE tags */
|
||||
if ( $values[ 'vle' ] and $replacement and isset( \IPS\Request::i(
|
||||
)->cookie[ 'vle_editor' ] ) and \IPS\Request::i()->cookie[ 'vle_editor' ] and \IPS\Member::loggedIn(
|
||||
)->hasAcpRestriction( 'core', 'languages', 'lang_words' )
|
||||
)
|
||||
if ( $values[ 'vle' ] and $replacement and static::vleActive() )
|
||||
{
|
||||
$replacement = "#VLE#{$values['key']}#!#[{$replacement}]#!##";
|
||||
$replacement = "#VLE#{$values['key']}#!#";
|
||||
}
|
||||
|
||||
if ( isset( $values[ 'options' ][ 'returnInto' ] ) )
|
||||
@@ -2106,4 +2141,25 @@ class _Lang extends \IPS\Node\Model
|
||||
$form->add( new \IPS\Helpers\Form\Translatable( 'word_custom', NULL, FALSE, array( 'textArea' => TRUE ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the visual language editor active?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function vleActive(): bool
|
||||
{
|
||||
return isset( \IPS\Request::i()->cookie[ 'vle_editor' ] ) and \IPS\Request::i()->cookie[ 'vle_editor' ] and \IPS\Member::loggedIn()->hasAcpRestriction( 'core', 'languages', 'lang_words' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of keys > values for the VLE
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function vleForJson(): array
|
||||
{
|
||||
/* if we're using IN_DEV, this is all of the strings, so... good luck I guess */
|
||||
return $this->words;
|
||||
}
|
||||
}
|
||||
@@ -550,4 +550,46 @@ class _Twitter extends \IPS\Login\Handler\OAuth1
|
||||
|
||||
return $this->_sendRequest( 'post', \IPS\Http\Url::external('https://upload.twitter.com/1.1/media/upload.json'), array(), $accessToken, $accessTokenSecret, array(), array( $mimeBoundary, $data ) )->decodeJson();
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Set whether this node is enabled
|
||||
*
|
||||
* @param bool|int $enabled Whether to set it enabled or disabled
|
||||
* @return void
|
||||
*/
|
||||
protected function set__enabled( $enabled )
|
||||
{
|
||||
parent::set__enabled( $enabled );
|
||||
$this->_checkSocialPromotes();
|
||||
}
|
||||
|
||||
/**
|
||||
* [ActiveRecord] Delete Record
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
parent::delete();
|
||||
$this->_checkSocialPromotes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable social promotes Twitter method if all Twitter login handlers are disabled
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function _checkSocialPromotes()
|
||||
{
|
||||
try
|
||||
{
|
||||
\IPS\Db::i()->select( '*', 'core_login_methods', array( 'login_classname=? AND login_id!=?', 'IPS\Login\Handler\OAuth1\Twitter', $this->_id ) )->first();
|
||||
}
|
||||
catch( \UnderflowException $e )
|
||||
{
|
||||
$promoter = \IPS\core\Promote::getPromoter('Twitter');
|
||||
$promoter->enabled = FALSE;
|
||||
$promoter->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -405,7 +405,7 @@ class _Login
|
||||
/* Can we get a physical location */
|
||||
try
|
||||
{
|
||||
$location = \IPS\GeoLocation::getByIp( \IPS\Request::i()->ipAddress() );
|
||||
$location = \IPS\GeoLocation::getRequesterLocation();
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
|
||||
@@ -551,6 +551,12 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
*/
|
||||
public function toggle( $enabled )
|
||||
{
|
||||
/* This handler is deprecated, so if it's already disabled, don't allow it to be re-enabled */
|
||||
if( !$this->isEnabled() )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if ( $enabled )
|
||||
{
|
||||
static::verifyApiKey( \IPS\Settings::i()->authy_key );
|
||||
@@ -566,6 +572,11 @@ class _Handler extends \IPS\MFA\MFAHandler
|
||||
*/
|
||||
public function acpSettings()
|
||||
{
|
||||
if( !$this->isEnabled() )
|
||||
{
|
||||
\IPS\Output::i()->error( 'authy_deprecated_message', '2C345/3' );
|
||||
}
|
||||
|
||||
$form = new \IPS\Helpers\Form;
|
||||
|
||||
$form->add( new \IPS\Helpers\Form\Text( 'authy_key', \IPS\Settings::i()->authy_key, TRUE, array(), function( $val ) {
|
||||
|
||||
@@ -49,7 +49,8 @@ abstract class _MFAHandler
|
||||
return array(
|
||||
'authy' => new \IPS\MFA\Authy\Handler(),
|
||||
'google' => new \IPS\MFA\GoogleAuthenticator\Handler(),
|
||||
'questions' => new \IPS\MFA\SecurityQuestions\Handler()
|
||||
'questions' => new \IPS\MFA\SecurityQuestions\Handler(),
|
||||
'verify' => new \IPS\MFA\Verify\Handler()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @brief Verify Exception
|
||||
* @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
|
||||
* @subpackage
|
||||
* @since 4/4/2023
|
||||
*/
|
||||
|
||||
namespace IPS\MFA\Verify;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
class _Exception extends \DomainException
|
||||
{
|
||||
const EXPIRED_CODE = 20404;
|
||||
|
||||
public function __construct( string $message = "", int $code = 0, ?\Throwable $previous = null )
|
||||
{
|
||||
switch( $code )
|
||||
{
|
||||
case static::EXPIRED_CODE:
|
||||
$message = 'verify_mfa_reused_code';
|
||||
break;
|
||||
}
|
||||
|
||||
parent::__construct( $message, $code, $previous );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,545 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @brief Multi Factor Authentication Handler for Verify
|
||||
* @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
|
||||
* @subpackage
|
||||
* @since 3/29/2023
|
||||
*/
|
||||
|
||||
namespace IPS\MFA\Verify;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
|
||||
use IPS\MFA\verify\Exception;
|
||||
|
||||
if( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
class _Handler extends \IPS\MFA\MFAHandler
|
||||
{
|
||||
/**
|
||||
* @brief Key
|
||||
*/
|
||||
protected $key = 'verify';
|
||||
|
||||
/**
|
||||
* Handler is enabled
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isEnabled()
|
||||
{
|
||||
return \IPS\Settings::i()->verify_enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Member *can* use this handler (even if they have not yet configured it)
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function memberCanUseHandler( \IPS\Member $member )
|
||||
{
|
||||
return \IPS\Settings::i()->verify_groups == '*' or $member->inGroup( explode( ',', \IPS\Settings::i()->verify_groups ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Member has configured this handler
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function memberHasConfiguredHandler( \IPS\Member $member )
|
||||
{
|
||||
return isset( $member->mfa_details['verify'] ) and $member->mfa_details['verify']['setup'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a setup screen
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param bool $showingMultipleHandlers Set to TRUE if multiple options are being displayed
|
||||
* @param \IPS\Http\Url $url URL for page
|
||||
* @return string
|
||||
*/
|
||||
public function configurationScreen( \IPS\Member $member, $showingMultipleHandlers, \IPS\Http\Url $url )
|
||||
{
|
||||
$mfaDetails = $member->mfa_details;
|
||||
|
||||
/* Starting again? */
|
||||
if ( isset( $mfaDetails['verify']['pendingId'] ) and isset( \IPS\Request::i()->_new ) )
|
||||
{
|
||||
unset( $mfaDetails['verify']['pendingId'] );
|
||||
$member->mfa_details = $mfaDetails;
|
||||
$member->save();
|
||||
}
|
||||
|
||||
/* If we have already entered our phone number, ask for the code */
|
||||
if ( isset( $mfaDetails['verify'] ) and isset( $mfaDetails['verify']['pendingId'] ) and !isset( $_SESSION['verifyConfigureError'] ) )
|
||||
{
|
||||
/* Asking for a text or call instead? */
|
||||
$availableMethods = explode( ',', \IPS\Settings::i()->verify_setup );
|
||||
if ( isset( \IPS\Request::i()->verify_method ) and $mfaDetails['verify']['setupMethod'] == 'verify' and \in_array( \IPS\Request::i()->verify_method, $availableMethods ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
/* Verify with the API */
|
||||
$response = static::_api( "Services/" . \IPS\Settings::i()->verify_service_sid . "/VerificationCheck", 'post', array(
|
||||
'VerificationSid' => $mfaDetails['verify']['pendingId'],
|
||||
'Code' => \IPS\Request::i()->verify_auth_code
|
||||
) );
|
||||
|
||||
if( $response['status'] == 'approved' )
|
||||
{
|
||||
/* Update details */
|
||||
$mfaDetails['verify']['setupMethod'] = \IPS\Request::i()->verify_method;
|
||||
$member->mfa_details = $mfaDetails;
|
||||
$member->save();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \IPS\MFA\Verify\Exception( 'verify_mfa_invalid_code' );
|
||||
}
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'verify' );
|
||||
$_SESSION['verifyAuthError'] = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/* Display */
|
||||
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->verifyAuthenticate( $mfaDetails['verify']['setupMethod'], TRUE, $_SESSION['verifyAuthError'] ?? 'verify_error', TRUE, explode( ',', \IPS\Settings::i()->verify_setup ), $url );
|
||||
}
|
||||
else
|
||||
{
|
||||
/* If they have used their allowed attempts, make them wait */
|
||||
if ( isset( $mfaDetails['verify'] ) and isset( $mfaDetails['verify']['changeAttempts'] ) and $mfaDetails['verify']['changeAttempts'] >= \IPS\Settings::i()->verify_setup_tries )
|
||||
{
|
||||
$lockEndTime = $mfaDetails['verify']['lastChangeAttempt'] + ( \IPS\Settings::i()->verify_setup_lockout * 3600 );
|
||||
if ( $lockEndTime < time() )
|
||||
{
|
||||
$mfaDetails['verify']['changeAttempts'] = 0;
|
||||
$member->mfa_details = $mfaDetails;
|
||||
$member->save();
|
||||
}
|
||||
else
|
||||
{
|
||||
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->verifySetupLockout( $showingMultipleHandlers, \IPS\DateTime::ts( $lockEndTime ) );
|
||||
}
|
||||
}
|
||||
|
||||
/* Otherwise show the form */
|
||||
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->verifySetup( \IPS\Request::i()->countryCode ?? \IPS\Helpers\Form\Address::calculateDefaultCountry(), \IPS\Request::i()->phoneNumber ?? '', $showingMultipleHandlers, explode( ',', \IPS\Settings::i()->verify_setup ), $_SESSION['verifyConfigureError'] ?? NULL );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit configuration screen. Return TRUE if was accepted
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function configurationScreenSubmit( \IPS\Member $member )
|
||||
{
|
||||
$mfaDetails = $member->mfa_details;
|
||||
|
||||
/* If we've enterred a code, verify it */
|
||||
if ( isset( $mfaDetails['verify'] ) and isset( $mfaDetails['verify']['pendingId'] ) and isset( \IPS\Request::i()->verify_auth_code ) )
|
||||
{
|
||||
$_SESSION['verifyAuthError'] = NULL;
|
||||
try
|
||||
{
|
||||
/* Verify with the API */
|
||||
$response = static::_api( "Services/" . \IPS\Settings::i()->verify_service_sid . "/VerificationCheck", 'post', array(
|
||||
'VerificationSid' => $mfaDetails['verify']['pendingId'],
|
||||
'Code' => \IPS\Request::i()->verify_auth_code
|
||||
) );
|
||||
|
||||
if( $response['status'] == 'approved' )
|
||||
{
|
||||
$mfaDetails['verify'] = array( 'id' => $response['to'], 'setup' => true );
|
||||
$member->mfa_details = $mfaDetails;
|
||||
$member->save();
|
||||
|
||||
$member->logHistory( 'core', 'mfa', array( 'handler' => $this->key, 'enable' => TRUE ) );
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \IPS\MFA\Verify\Exception( 'verify_mfa_invalid_code' );
|
||||
}
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'verify' );
|
||||
$_SESSION['verifyAuthError'] = $e->getMessage();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Otherwise we need to generate an ID */
|
||||
elseif ( \IPS\Request::i()->phoneNumber )
|
||||
{
|
||||
/* Do we need to wait a while? */
|
||||
if ( isset( $mfaDetails['verify'] ) and isset( $mfaDetails['verify']['changeAttempts'] ) and $mfaDetails['verify']['changeAttempts'] >= \IPS\Settings::i()->verify_setup_tries )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Reformat the phone number to E.164 */
|
||||
$countryCode = \substr( \IPS\Request::i()->countryCode, \strpos( \IPS\Request::i()->countryCode, "-" ) + 1 );
|
||||
$phoneNumber = "+" . $countryCode . \IPS\Request::i()->phoneNumber;
|
||||
|
||||
/* Call verify */
|
||||
$availableMethods = explode( ',', \IPS\Settings::i()->verify_setup );
|
||||
$method = ( isset( \IPS\Request::i()->method ) and \in_array( \IPS\Request::i()->method, $availableMethods ) ) ? \IPS\Request::i()->method : array_shift( $availableMethods );
|
||||
$_SESSION['verifyConfigureError'] = NULL;
|
||||
|
||||
$channel = ( \IPS\Request::i()->method == 'phone' ) ? 'call' : \IPS\Request::i()->method;
|
||||
|
||||
try
|
||||
{
|
||||
$response = static::_api( "Services/" . \IPS\Settings::i()->verify_service_sid . "/Verifications", 'post', array(
|
||||
'To' => $phoneNumber,
|
||||
'Channel' => $channel
|
||||
) );
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'verify' );
|
||||
$_SESSION['verifyConfigureError'] = $e->getMessage();
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Log the details */
|
||||
$mfaDetails['verify']['pendingId'] = $response['sid'];
|
||||
if ( !isset( $mfaDetails['verify']['changeAttempts'] ) )
|
||||
{
|
||||
$mfaDetails['verify']['changeAttempts'] = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
$mfaDetails['verify']['changeAttempts']++;
|
||||
}
|
||||
$mfaDetails['verify']['lastChangeAttempt'] = time();
|
||||
if ( !isset( $mfaDetails['verify']['setup'] ) )
|
||||
{
|
||||
$mfaDetails['verify']['setup'] = false;
|
||||
}
|
||||
$mfaDetails['verify']['setupMethod'] = $method;
|
||||
$member->mfa_details = $mfaDetails;
|
||||
$member->save();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the form for a member to authenticate
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @param \IPS\Http\Url $url URL for page
|
||||
* @return string
|
||||
*/
|
||||
public function authenticationScreen( \IPS\Member $member, \IPS\Http\Url $url )
|
||||
{
|
||||
$mfaDetails = $member->mfa_details;
|
||||
$availableMethods = explode( ',', \IPS\Settings::i()->verify_method );
|
||||
|
||||
/* If we sent a code, but it was less than one minute ago, log a failure and reset */
|
||||
if ( isset( $mfaDetails['verify']['sent'] ) and $mfaDetails['verify']['sent']['time'] < ( time() - 60 ) and !isset( \IPS\Request::i()->verify_auth_code ) )
|
||||
{
|
||||
unset( $mfaDetails['verify']['sent'] );
|
||||
$member->mfa_details = $mfaDetails;
|
||||
$member->failed_mfa_attempts++;
|
||||
$member->save();
|
||||
}
|
||||
|
||||
/* If text message is the only available option, or we have chosen that option, do that... */
|
||||
$selectedMethod = \IPS\Request::i()->verify_method ?? NULL;
|
||||
if( $selectedMethod === NULL )
|
||||
{
|
||||
if( isset( \IPS\Request::i()->verify_auth_code ) and isset( $mfaDetails['verify']['sent'] ) )
|
||||
{
|
||||
$selectedMethod = $mfaDetails['verify']['sent']['method'];
|
||||
}
|
||||
elseif( \count( $availableMethods ) == 1 )
|
||||
{
|
||||
$selectedMethod = $availableMethods[0];
|
||||
}
|
||||
}
|
||||
|
||||
/* Send the text if we haven't already */
|
||||
if ( !isset( $mfaDetails['verify']['sent'] ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
$channel = $selectedMethod == 'phone' ? 'call' : $selectedMethod;
|
||||
$response = static::_api( "Services/" . \IPS\Settings::i()->verify_service_sid . "/Verifications", 'post', array(
|
||||
'To' => $mfaDetails['verify']['id'],
|
||||
'Channel' => $channel
|
||||
) );
|
||||
|
||||
/* Update details */
|
||||
$mfaDetails['verify']['sent'] = array( 'method' => $selectedMethod, 'time' => time() );
|
||||
$member->mfa_details = $mfaDetails;
|
||||
$member->save();
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'verify' );
|
||||
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->verifyError( $e->getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
/* Show screen */
|
||||
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->verifyAuthenticate( $selectedMethod ?: 'choose', ( isset( $mfaDetails['verify']['sent'] ) and $mfaDetails['verify']['sent']['method'] == $selectedMethod ), $_SESSION['verifyAuthError'] ?? 'verify_error', $mfaDetails['verify']['setup'] ?? FALSE, $availableMethods, $url );
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit authentication screen. Return TRUE if was accepted
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @return string
|
||||
*/
|
||||
public function authenticationScreenSubmit( \IPS\Member $member )
|
||||
{
|
||||
$mfaDetails = $member->mfa_details;
|
||||
|
||||
$_SESSION['verifyAuthError'] = NULL;
|
||||
|
||||
try
|
||||
{
|
||||
if ( isset( \IPS\Request::i()->verify_auth_code ) )
|
||||
{
|
||||
/* Verify with the API */
|
||||
$response = static::_api( "Services/" . \IPS\Settings::i()->verify_service_sid . "/VerificationCheck", 'post', array(
|
||||
'To' => $mfaDetails['verify']['id'],
|
||||
'Code' => \IPS\Request::i()->verify_auth_code
|
||||
) );
|
||||
|
||||
if( $response['status'] == 'approved' )
|
||||
{
|
||||
$member->mfa_details = $mfaDetails;
|
||||
$member->save();
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \IPS\MFA\Verify\Exception( 'verify_mfa_invalid_code' );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'verify' );
|
||||
$_SESSION['verifyAuthError'] = $e->getMessage();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle
|
||||
*
|
||||
* @param bool $enabled On/Off
|
||||
* @return bool
|
||||
*/
|
||||
public function toggle( $enabled )
|
||||
{
|
||||
if ( $enabled )
|
||||
{
|
||||
static::verifyApiKeys( \IPS\Settings::i()->verify_sid, \IPS\Settings::i()->verify_token );
|
||||
}
|
||||
|
||||
\IPS\Settings::i()->changeValues( array( 'verify_enabled' => $enabled ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* ACP Settings
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function acpSettings()
|
||||
{
|
||||
$form = new \IPS\Helpers\Form;
|
||||
|
||||
$form->add( new \IPS\Helpers\Form\Text( 'verify_sid', \IPS\Settings::i()->verify_sid, TRUE, array(), NULL, NULL, \IPS\Member::loggedIn()->language()->addToStack( 'verify_sid_suffix' ) ) );
|
||||
$form->add( new \IPS\Helpers\Form\Text( 'verify_token', \IPS\Settings::i()->verify_token, TRUE ) );
|
||||
$form->add( new \IPS\Helpers\Form\CheckboxSet( 'verify_groups', \IPS\Settings::i()->verify_groups == '*' ? '*' : explode( ',', \IPS\Settings::i()->verify_groups ), FALSE, array(
|
||||
'multiple' => TRUE,
|
||||
'options' => array_combine( array_keys( \IPS\Member\Group::groups() ), array_map( function( $_group ) { return (string) $_group; }, \IPS\Member\Group::groups() ) ),
|
||||
'unlimited' => '*',
|
||||
'unlimitedLang' => 'everyone',
|
||||
'impliedUnlimited' => TRUE
|
||||
) ) );
|
||||
|
||||
$form->addHeader('verify_setup_header');
|
||||
$form->add( new \IPS\Helpers\Form\CheckboxSet( 'verify_setup', explode( ',', \IPS\Settings::i()->verify_setup ), TRUE, array( 'options' => array(
|
||||
'sms' => 'verify_method_sms',
|
||||
'phone' => 'verify_method_phone',
|
||||
'whatsapp' => 'verify_method_whatsapp'
|
||||
) ) ) );
|
||||
$form->add( new \IPS\Helpers\Form\Custom( 'verify_setup_protection', array( \IPS\Settings::i()->verify_setup_tries, \IPS\Settings::i()->verify_setup_lockout ), FALSE, array(
|
||||
'getHtml' => function( $field ) {
|
||||
return \IPS\Theme::i()->getTemplate('settings')->verifySetupProtection( $field->value );
|
||||
}
|
||||
) ) );
|
||||
|
||||
$form->addHeader('verify_authenticate_header');
|
||||
$form->add( new \IPS\Helpers\Form\CheckboxSet( 'verify_method', explode( ',', \IPS\Settings::i()->verify_method ), TRUE, array(
|
||||
'options' => array(
|
||||
'sms' => 'verify_method_sms',
|
||||
'phone' => 'verify_method_phone',
|
||||
'whatsapp' => 'verify_method_whatsapp'
|
||||
)
|
||||
) ) );
|
||||
|
||||
if ( $values = $form->values() )
|
||||
{
|
||||
try
|
||||
{
|
||||
$data = static::verifyApiKeys( $values['verify_sid'], $values['verify_token'] );
|
||||
|
||||
/* If we do not have any services yet, create one */
|
||||
if( !\count( $data['services'] ) )
|
||||
{
|
||||
/* Temporarily set the tokens so that we can properly make the next call */
|
||||
\IPS\Settings::i()->verify_sid = $values['verify_sid'];
|
||||
\IPS\Settings::i()->verify_token = $values['verify_token'];
|
||||
$service = static::_api( "Services", 'post', array(
|
||||
'FriendlyName' => 'IPS MFA'
|
||||
) );
|
||||
|
||||
$values['verify_service_sid'] = $service['sid'];
|
||||
}
|
||||
else
|
||||
{
|
||||
$values['verify_service_sid'] = $data['services'][0]['sid'];
|
||||
}
|
||||
|
||||
$values['verify_groups'] = ( $values['verify_groups'] == '*' ) ? '*' : implode( ',', $values['verify_groups'] );
|
||||
$values['verify_setup'] = isset( $values['verify_setup'] ) ? implode( ',', $values['verify_setup'] ) : '';
|
||||
$values['verify_setup_tries'] = $values['verify_setup_protection'][0];
|
||||
$values['verify_setup_lockout'] = $values['verify_setup_protection'][1];
|
||||
unset( $values['verify_setup_protection'] );
|
||||
$values['verify_method'] = isset( $values['verify_method'] ) ? implode( ',', $values['verify_method'] ) : '';
|
||||
$form->saveAsSettings( $values );
|
||||
|
||||
\IPS\Session::i()->log( 'acplogs__mfa_handler_enabled', array( "mfa_verify_title" => TRUE ) );
|
||||
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=settings&controller=mfa' ), 'saved' );
|
||||
}
|
||||
catch( \DomainException $e )
|
||||
{
|
||||
$form->error = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
return (string) $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify API Keys
|
||||
*
|
||||
* @param string $sid The Verify SID
|
||||
* @param string $token The Verify Auth Token
|
||||
* @return array
|
||||
* @throws \DomainException
|
||||
*/
|
||||
public static function verifyApiKeys( string $sid, string $token ) : array
|
||||
{
|
||||
try
|
||||
{
|
||||
$response = \IPS\Http\Url::external("https://verify.twilio.com/v2/Services" )
|
||||
->request()
|
||||
->login( $sid, $token )
|
||||
->get()
|
||||
->decodeJson();
|
||||
|
||||
if( !isset( $response['services'] ) )
|
||||
{
|
||||
throw new \DomainException( $response['message'] );
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
{
|
||||
throw new \DomainException( $e->getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If member has configured this handler, disable it
|
||||
*
|
||||
* @param \IPS\Member $member The member
|
||||
* @return bool
|
||||
*/
|
||||
public function disableHandlerForMember( \IPS\Member $member )
|
||||
{
|
||||
$mfaDetails = $member->mfa_details;
|
||||
|
||||
if ( isset( $mfaDetails['verify'] ) )
|
||||
{
|
||||
unset( $mfaDetails['verify'] );
|
||||
$member->mfa_details = $mfaDetails;
|
||||
$member->save();
|
||||
|
||||
/* Log MFA Disable */
|
||||
$member->logHistory( 'core', 'mfa', array( 'handler' => $this->key, 'enable' => FALSE ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make API Call
|
||||
*
|
||||
* @param string $endpoint The endpoint to call
|
||||
* @param string $method 'get' or 'post'
|
||||
* @param array|null $data Post data or additional query string parameters
|
||||
* @return array
|
||||
* @throws \IPS\MFA\Verify\Exception
|
||||
*/
|
||||
protected static function _api( string $endpoint, string $method='get', ?array $data=NULL ) : array
|
||||
{
|
||||
$url = \IPS\Http\Url::external("https://verify.twilio.com/v2/{$endpoint}" );
|
||||
|
||||
if ( $method == 'get' )
|
||||
{
|
||||
$response = $url->setQueryString( $data )
|
||||
->request()
|
||||
->login( \IPS\Settings::i()->verify_sid, \IPS\Settings::i()->verify_token )
|
||||
->get();
|
||||
}
|
||||
else
|
||||
{
|
||||
$response = $url->request()
|
||||
->login( \IPS\Settings::i()->verify_sid, \IPS\Settings::i()->verify_token )
|
||||
->post( $data );
|
||||
|
||||
}
|
||||
|
||||
$response = $response->decodeJson();
|
||||
|
||||
if( isset( $response['code'] ) AND isset( $response['message'] ) )
|
||||
{
|
||||
throw new \IPS\MFA\Verify\Exception( $response['message'], $response['code'] );
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
+256
-172
@@ -539,202 +539,240 @@ class _Club extends \IPS\Patterns\ActiveRecord implements \IPS\Content\Embeddabl
|
||||
{
|
||||
$form->add( new \IPS\Helpers\Form\YesNo( 'club_rules_reacknowledge', FALSE, FALSE, [], NULL, NULL, NULL, 'club_rules_reacknowledge' ) );
|
||||
}
|
||||
|
||||
if ( $values = $form->values() )
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process Club Form
|
||||
*
|
||||
* @param $values
|
||||
* @param bool $acp TRUE if editing in the ACP
|
||||
* @param bool $new TRUE if creating new
|
||||
* @param array|null $availableTypes If creating new, the available types
|
||||
* @return \IPS\Helpers\Form|NULL
|
||||
*/
|
||||
public function processForm($values, bool $acp, bool $new = FALSE, array $availableTypes = NULL): ?\IPS\Helpers\Form
|
||||
{
|
||||
$this->name = $values['club_name'];
|
||||
|
||||
/* If there is only one type available, set it. */
|
||||
if( \is_array( $availableTypes ) AND \count( $availableTypes ) == 1 )
|
||||
{
|
||||
$this->name = $values['club_name'];
|
||||
$values['club_type'] = key( $availableTypes );
|
||||
}
|
||||
|
||||
/* If there is only one type available, set it. */
|
||||
if( \is_array( $availableTypes ) AND \count( $availableTypes ) == 1 )
|
||||
{
|
||||
$values['club_type'] = key( $availableTypes );
|
||||
}
|
||||
|
||||
$needToUpdatePermissions = FALSE;
|
||||
if ( $acp )
|
||||
{
|
||||
if ( $this->type != $values['club_type'] )
|
||||
{
|
||||
$this->type = $values['club_type'];
|
||||
$needToUpdatePermissions = TRUE;
|
||||
}
|
||||
if ( $this->owner != $values['club_owner'] )
|
||||
{
|
||||
$this->owner = $values['club_owner'];
|
||||
$this->addMember( $values['club_owner'], \IPS\Member\Club::STATUS_LEADER, TRUE );
|
||||
}
|
||||
}
|
||||
elseif ( $new )
|
||||
$needToUpdatePermissions = FALSE;
|
||||
if ( $acp )
|
||||
{
|
||||
if ( $this->type != $values['club_type'] )
|
||||
{
|
||||
$this->type = $values['club_type'];
|
||||
$this->owner = \IPS\Member::loggedIn();
|
||||
$needToUpdatePermissions = TRUE;
|
||||
}
|
||||
|
||||
$this->about = $values['club_about'];
|
||||
$this->profile_photo = (string) $values['club_profile_photo'];
|
||||
|
||||
if( $values['club_profile_photo'] )
|
||||
if ( $this->owner != $values['club_owner'] )
|
||||
{
|
||||
$this->profile_photo_uncropped = (string) $values['club_profile_photo'];
|
||||
$this->owner = $values['club_owner'];
|
||||
$this->addMember( $values['club_owner'], Club::STATUS_LEADER, TRUE );
|
||||
|
||||
/* Update purchases for commission */
|
||||
if( \IPS\Application::appIsEnabled( 'nexus' ) and \IPS\Settings::i()->clubs_paid_on )
|
||||
{
|
||||
\IPS\Db::i()->update( 'nexus_purchases', array( 'ps_pay_to' => $this->owner ), array( "ps_pay_to IS NOT NULL and ps_app=? and ps_type=? and ps_item_id=?", 'core', 'club', $this->id ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $values['club_location'] ) )
|
||||
}
|
||||
elseif ( $new )
|
||||
{
|
||||
$this->type = $values['club_type'];
|
||||
$this->owner = \IPS\Member::loggedIn();
|
||||
}
|
||||
|
||||
$this->about = $values['club_about'];
|
||||
$this->profile_photo = (string) $values['club_profile_photo'];
|
||||
|
||||
if( $values['club_profile_photo'] )
|
||||
{
|
||||
$this->profile_photo_uncropped = (string) $values['club_profile_photo'];
|
||||
}
|
||||
|
||||
if ( isset( $values['club_location'] ) )
|
||||
{
|
||||
$this->location_json = json_encode( $values['club_location'] );
|
||||
if ( $values['club_location']->lat and $values['club_location']->long )
|
||||
{
|
||||
$this->location_json = json_encode( $values['club_location'] );
|
||||
if ( $values['club_location']->lat and $values['club_location']->long )
|
||||
{
|
||||
$this->location_lat = $values['club_location']->lat;
|
||||
$this->location_long = $values['club_location']->long;
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->location_lat = NULL;
|
||||
$this->location_long = NULL;
|
||||
}
|
||||
$this->location_lat = $values['club_location']->lat;
|
||||
$this->location_long = $values['club_location']->long;
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->location_json = NULL;
|
||||
$this->location_lat = NULL;
|
||||
$this->location_long = NULL;
|
||||
}
|
||||
|
||||
if ( \IPS\Application::appIsEnabled( 'nexus' ) and \IPS\Settings::i()->clubs_paid_on and \IPS\Member::loggedIn()->group['gbw_paid_clubs'] )
|
||||
{
|
||||
switch ( $values['club_membership_fee'] )
|
||||
{
|
||||
case 'free':
|
||||
$this->fee = NULL;
|
||||
$this->renewal_term = 0;
|
||||
$this->renewal_units = NULL;
|
||||
$this->renewal_price = NULL;
|
||||
break;
|
||||
|
||||
case 'paid':
|
||||
$this->fee = json_encode( $values['club_fee'] );
|
||||
if ( $values['club_renewals'] and $values['club_renewal_term'] )
|
||||
{
|
||||
$term = $values['club_renewal_term']->getTerm();
|
||||
$this->renewal_term = $term['term'];
|
||||
$this->renewal_units = $term['unit'];
|
||||
$this->renewal_price = json_encode( $values['club_renewal_term']->cost );
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->renewal_term = 0;
|
||||
$this->renewal_units = NULL;
|
||||
$this->renewal_price = NULL;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->location_json = NULL;
|
||||
$this->location_lat = NULL;
|
||||
$this->location_long = NULL;
|
||||
}
|
||||
|
||||
if ( array_key_exists( 'club_show_membertab', $values ) )
|
||||
if ( \IPS\Application::appIsEnabled( 'nexus' ) and \IPS\Settings::i()->clubs_paid_on and \IPS\Member::loggedIn()->group['gbw_paid_clubs'] )
|
||||
{
|
||||
switch ( $values['club_membership_fee'] )
|
||||
{
|
||||
$this->show_membertab = $values['club_show_membertab'];
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Default is "Everybody" */
|
||||
$this->show_membertab = "nonmember";
|
||||
}
|
||||
case 'free':
|
||||
$this->fee = NULL;
|
||||
$this->renewal_term = 0;
|
||||
$this->renewal_units = NULL;
|
||||
$this->renewal_price = NULL;
|
||||
break;
|
||||
|
||||
$this->save();
|
||||
|
||||
if ( $values['show_rules'] )
|
||||
{
|
||||
$this->rules = $values['club_rules'];
|
||||
\IPS\File::claimAttachments( ( $new ) ? "club-rules-new" : "club-rules-{$this->id}", $this->id, NULL, 'rules' );
|
||||
$this->rules_required = (bool) $values['club_rules_required'];
|
||||
|
||||
/* Do we need to reset the acknowledge flags? */
|
||||
if ( !$new AND \array_key_exists( 'club_rules_reacknowledge', $values ) )
|
||||
{
|
||||
if ( $values['club_rules_reacknowledge'] )
|
||||
case 'paid':
|
||||
$this->fee = json_encode( $values['club_fee'] );
|
||||
|
||||
if ( $values['club_renewals'] and $values['club_renewal_term'] )
|
||||
{
|
||||
\IPS\Db::i()->update( 'core_clubs_memberships', array( "rules_acknowledged" => FALSE ), array( "club_id=?", $this->id ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->rules = NULL;
|
||||
$this->rules_required = FALSE;
|
||||
|
||||
if ( !$new )
|
||||
{
|
||||
/* If this isn't a new club, update all memberships in case they decide to re-add rules later on */
|
||||
\IPS\Db::i()->update( "core_clubs_memberships", array( 'rules_acknowledged' => FALSE ), array( "club_id=?", $this->id ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( $new )
|
||||
{
|
||||
$this->addMember( \IPS\Member::loggedIn(), \IPS\Member\Club::STATUS_LEADER );
|
||||
$this->acknowledgeRules( \IPS\Member::loggedIn() );
|
||||
}
|
||||
$this->recountMembers();
|
||||
|
||||
$customFieldValues = array();
|
||||
foreach ( \IPS\Member\Club\CustomField::roots() as $field )
|
||||
{
|
||||
if ( isset( $values["core_clubfield_{$field->id}"] ) )
|
||||
{
|
||||
$helper = $field->buildHelper();
|
||||
|
||||
if ( $helper instanceof \IPS\Helpers\Form\Upload )
|
||||
{
|
||||
$customFieldValues[ "field_{$field->id}" ] = (string) $values["core_clubfield_{$field->id}"];
|
||||
$term = $values['club_renewal_term']->getTerm();
|
||||
$this->renewal_term = $term['term'];
|
||||
$this->renewal_units = $term['unit'];
|
||||
$this->renewal_price = json_encode( $values['club_renewal_term']->cost );
|
||||
}
|
||||
else
|
||||
{
|
||||
$customFieldValues[ "field_{$field->id}" ] = $helper::stringValue( $values["core_clubfield_{$field->id}"] );
|
||||
$this->renewal_term = 0;
|
||||
$this->renewal_units = NULL;
|
||||
$this->renewal_price = NULL;
|
||||
}
|
||||
|
||||
if ( $field->type === 'Editor' )
|
||||
{
|
||||
$field->claimAttachments( $this->id );
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
if ( \count( $customFieldValues ) )
|
||||
{
|
||||
$customFieldValues['club_id'] = $this->id;
|
||||
\IPS\Db::i()->insert( 'core_clubs_fieldvalues', $customFieldValues, TRUE );
|
||||
}
|
||||
|
||||
if ( $needToUpdatePermissions )
|
||||
{
|
||||
foreach ( $this->nodes() as $node )
|
||||
{
|
||||
try
|
||||
{
|
||||
$nodeClass = $node['node_class'];
|
||||
$node = $nodeClass::load( $node['node_id'] );
|
||||
$node->setPermissionsToClub( $this );
|
||||
}
|
||||
catch ( \Exception $e ) { }
|
||||
}
|
||||
}
|
||||
|
||||
if( $new and \IPS\Settings::i()->clubs_require_approval and !$this->approved )
|
||||
{
|
||||
$this->sendModeratorApprovalNotification();
|
||||
}
|
||||
|
||||
if( $new and $this->approved )
|
||||
{
|
||||
\IPS\Api\Webhook::fire( 'club_created', $this );
|
||||
}
|
||||
else if( !$new AND isset( $this->changed['approved'] ) )
|
||||
{
|
||||
$this->onApprove();
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return $form;
|
||||
|
||||
if ( array_key_exists( 'club_show_membertab', $values ) )
|
||||
{
|
||||
$this->show_membertab = $values['club_show_membertab'];
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Default is "Everybody" */
|
||||
$this->show_membertab = "nonmember";
|
||||
}
|
||||
|
||||
$this->save();
|
||||
|
||||
if ( $values['show_rules'] )
|
||||
{
|
||||
$this->rules = $values['club_rules'];
|
||||
\IPS\File::claimAttachments( ( $new ) ? "club-rules-new" : "club-rules-{$this->id}", $this->id, NULL, 'rules' );
|
||||
$this->rules_required = (bool) $values['club_rules_required'];
|
||||
|
||||
/* Do we need to reset the acknowledge flags? */
|
||||
if ( !$new AND \array_key_exists( 'club_rules_reacknowledge', $values ) )
|
||||
{
|
||||
if ( $values['club_rules_reacknowledge'] )
|
||||
{
|
||||
\IPS\Db::i()->update( 'core_clubs_memberships', array( "rules_acknowledged" => FALSE ), array( "club_id=?", $this->id ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->rules = NULL;
|
||||
$this->rules_required = FALSE;
|
||||
|
||||
if ( !$new )
|
||||
{
|
||||
/* If this isn't a new club, update all memberships in case they decide to re-add rules later on */
|
||||
\IPS\Db::i()->update( "core_clubs_memberships", array( 'rules_acknowledged' => FALSE ), array( "club_id=?", $this->id ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( $new )
|
||||
{
|
||||
$this->addMember( \IPS\Member::loggedIn(), Club::STATUS_LEADER );
|
||||
$this->acknowledgeRules( \IPS\Member::loggedIn() );
|
||||
}
|
||||
$this->recountMembers();
|
||||
|
||||
$customFieldValues = array();
|
||||
foreach ( \IPS\Member\Club\CustomField::roots() as $field )
|
||||
{
|
||||
if ( isset( $values["core_clubfield_{$field->id}"] ) )
|
||||
{
|
||||
$helper = $field->buildHelper();
|
||||
|
||||
if ( $helper instanceof \IPS\Helpers\Form\Upload )
|
||||
{
|
||||
$customFieldValues[ "field_{$field->id}" ] = (string) $values["core_clubfield_{$field->id}"];
|
||||
}
|
||||
else
|
||||
{
|
||||
$customFieldValues[ "field_{$field->id}" ] = $helper::stringValue( $values["core_clubfield_{$field->id}"] );
|
||||
}
|
||||
|
||||
if ( $field->type === 'Editor' )
|
||||
{
|
||||
$field->claimAttachments( $this->id );
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( \count( $customFieldValues ) )
|
||||
{
|
||||
$customFieldValues['club_id'] = $this->id;
|
||||
\IPS\Db::i()->insert( 'core_clubs_fieldvalues', $customFieldValues, TRUE );
|
||||
}
|
||||
|
||||
if ( $needToUpdatePermissions )
|
||||
{
|
||||
foreach ( $this->nodes() as $node )
|
||||
{
|
||||
try
|
||||
{
|
||||
$nodeClass = $node['node_class'];
|
||||
$node = $nodeClass::load( $node['node_id'] );
|
||||
$node->setPermissionsToClub( $this );
|
||||
}
|
||||
catch ( \Exception $e ) { }
|
||||
}
|
||||
}
|
||||
|
||||
if( $new and \IPS\Settings::i()->clubs_require_approval and !$this->approved )
|
||||
{
|
||||
$this->sendModeratorApprovalNotification();
|
||||
}
|
||||
|
||||
if( $new and $this->approved )
|
||||
{
|
||||
\IPS\Api\Webhook::fire( 'club_created', $this );
|
||||
}
|
||||
else if( !$new AND isset( $this->changed['approved'] ) )
|
||||
{
|
||||
$this->onApprove();
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect after save
|
||||
*
|
||||
* @param \IPS\Member\Club $old A clone of the club as it was before
|
||||
* @param \IPS\Member\Club\ $new The club now
|
||||
* @return array
|
||||
*/
|
||||
public static function renewalChanges(Club $old, Club $new)
|
||||
{
|
||||
$changes = array();
|
||||
|
||||
foreach ( array( 'renewal_term', 'renewal_units', 'renewal_price' ) as $k )
|
||||
{
|
||||
if ( $old->$k != $new->$k )
|
||||
{
|
||||
$changes[ $k ] = $old->$k;
|
||||
}
|
||||
}
|
||||
|
||||
return $changes;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2191,4 +2229,50 @@ class _Club extends \IPS\Patterns\ActiveRecord implements \IPS\Content\Embeddabl
|
||||
\IPS\Api\Webhook::fire( 'club_created', $this );
|
||||
$this->owner->achievementAction( 'core', 'NewClub', $this );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update existing purchases
|
||||
*
|
||||
* @param \IPS\nexus\Purchase $purchase The purchase
|
||||
* @param array $changes The old values
|
||||
* @param bool $cancelBillingAgreementIfNecessary If making changes to renewal terms, TRUE will cancel associated billing agreements. FALSE will skip that change
|
||||
* @return void
|
||||
*/
|
||||
public function updatePurchase( \IPS\nexus\Purchase $purchase, $changes=array(), $cancelBillingAgreementIfNecessary=FALSE )
|
||||
{
|
||||
$tax = NULL;
|
||||
if ( $purchase->tax )
|
||||
{
|
||||
try
|
||||
{
|
||||
$tax = \IPS\nexus\Tax::load( $purchase->tax );
|
||||
}
|
||||
catch ( \OutOfRangeException $e ) { }
|
||||
}
|
||||
|
||||
$currency = $purchase->renewal_currency ?: $purchase->member->defaultCurrency( );
|
||||
|
||||
$price = json_decode( $this->renewal_price, TRUE );
|
||||
|
||||
$purchase->renewals = new \IPS\nexus\Purchase\RenewalTerm(
|
||||
new \IPS\nexus\Money( $price[$currency]['amount'], $currency ),
|
||||
new \DateInterval( 'P' . $this->renewal_term . mb_strtoupper( $this->renewal_units ) ),
|
||||
$tax
|
||||
);
|
||||
|
||||
if ( $cancelBillingAgreementIfNecessary and $billingAgreement = $purchase->billing_agreement )
|
||||
{
|
||||
if ( array_key_exists( 'renewal_price', $changes ) and !empty( $changes['renewal_price'] ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
$billingAgreement->cancel();
|
||||
$billingAgreement->save();
|
||||
}
|
||||
catch ( \Exception $e ) { }
|
||||
}
|
||||
}
|
||||
|
||||
$purchase->save();
|
||||
}
|
||||
}
|
||||
@@ -262,7 +262,7 @@ class _Device extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
try
|
||||
{
|
||||
$location = \IPS\GeoLocation::getByIp( \IPS\Request::i()->ipAddress() );
|
||||
$location = \IPS\GeoLocation::getRequesterLocation();
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
|
||||
+334
-12
@@ -120,6 +120,8 @@ class _Member extends \IPS\Patterns\ActiveRecord
|
||||
'new_device_email' => 16777216, // Send an email when a new device is used to log in.
|
||||
'no_solved_reenage' => 33554432, // User does not want to get topic re-engagement emails
|
||||
'datalayer_pii_optout' => 67108864, // User has opted in to having their PII collected by datalayer
|
||||
'email_messages_bounce' => 134217728, // User's email keeps returning hard bounces so they need to change it
|
||||
// 268435456 - cookie_optout moved to cookie preferences
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -133,7 +135,6 @@ class _Member extends \IPS\Patterns\ActiveRecord
|
||||
'last_visit',
|
||||
'last_activity',
|
||||
'profilesync_lastsync',
|
||||
|
||||
];
|
||||
|
||||
/* !Follow */
|
||||
@@ -203,7 +204,12 @@ class _Member extends \IPS\Patterns\ActiveRecord
|
||||
|
||||
if ( !static::$loggedInMember->member_id and isset( \IPS\Request::i()->cookie['ipsTimezone'] ) )
|
||||
{
|
||||
static::$loggedInMember->timezone = \IPS\DateTime::getFixedTimezone( \IPS\Request::i()->cookie['ipsTimezone'] );
|
||||
/* Check for valid timezone identifier */
|
||||
$tz = \IPS\DateTime::getFixedTimezone( \IPS\Request::i()->cookie['ipsTimezone'] );
|
||||
if( \in_array( $tz, \DateTimeZone::listIdentifiers() ) )
|
||||
{
|
||||
static::$loggedInMember->timezone = $tz;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,6 +337,47 @@ class _Member extends \IPS\Patterns\ActiveRecord
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow optional cookies
|
||||
*
|
||||
* @param bool $allow
|
||||
* @return bool
|
||||
*/
|
||||
public function setAllowOptionalCookies( bool $allow = TRUE ): bool
|
||||
{
|
||||
$expire = ( new \IPS\DateTime )->add( new \DateInterval('P6M' ) );
|
||||
\IPS\Request::i()->setCookie( 'cookie_consent', 1, $expire, FALSE );
|
||||
|
||||
if( !$allow )
|
||||
{
|
||||
/* Remove optional cookies */
|
||||
foreach( \IPS\Request::i()->cookie as $cookieName => $value )
|
||||
{
|
||||
if( !\in_array( $cookieName, \IPS\Request::i()->getEssentialCookies() ) )
|
||||
{
|
||||
\IPS\Request::i()->setCookie( $cookieName, NULL);
|
||||
}
|
||||
}
|
||||
\IPS\Request::i()->setCookie( 'cookie_consent_optional', NULL );
|
||||
}
|
||||
else
|
||||
{
|
||||
\IPS\Request::i()->setCookie( 'cookie_consent_optional', 1, $expire, FALSE );
|
||||
}
|
||||
|
||||
return $allow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Are optional cookies allowed?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function get_optionalCookiesAllowed(): bool
|
||||
{
|
||||
return isset( \IPS\Request::i()->cookie[ 'cookie_consent' ] ) AND isset( \IPS\Request::i()->cookie[ 'cookie_consent_optional' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* [ActiveRecord] Delete Record
|
||||
*
|
||||
@@ -411,6 +458,7 @@ class _Member extends \IPS\Patterns\ActiveRecord
|
||||
/* @note The completed column is added in the 4.4.0 Beta 1 routine, so we need to verify the upgrade has been performed otherwise
|
||||
the auto-upgrader can replace this file before the column is added, and an SQL error can prevent the admin from completing the
|
||||
auto-upgrade process */
|
||||
$fireRegistrationCompletedWebhook = FALSE;
|
||||
if ( \IPS\Application::load( 'core' )->long_version > 104000 )
|
||||
{
|
||||
if( $this->completed AND ( !$this->name OR !$this->email ) )
|
||||
@@ -421,22 +469,32 @@ class _Member extends \IPS\Patterns\ActiveRecord
|
||||
elseif( !$this->completed AND $this->name AND $this->email )
|
||||
{
|
||||
$this->completed = TRUE;
|
||||
\IPS\Api\Webhook::fire( 'member_registration_complete', $this, $this->webhookFilters() );
|
||||
$fireRegistrationCompletedWebhook = TRUE;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* If the email was changed, make sure that the bounce flag is unset so the member no longer sees the error message */
|
||||
if ( isset( $changes['email'] ) )
|
||||
{
|
||||
$this->members_bitoptions['email_messages_bounce'] = 0;
|
||||
}
|
||||
|
||||
parent::save();
|
||||
|
||||
if ( $new )
|
||||
{
|
||||
$this->memberSync( 'onCreateAccount' );
|
||||
|
||||
/* Profile Fields */
|
||||
\IPS\Db::i()->insert( 'core_pfields_content', array( 'member_id' => $this->member_id ), TRUE );
|
||||
|
||||
$this->memberSync( 'onCreateAccount' );
|
||||
}
|
||||
else
|
||||
{
|
||||
if( $fireRegistrationCompletedWebhook )
|
||||
{
|
||||
\IPS\Api\Webhook::fire( 'member_registration_complete', $this, $this->webhookFilters() );
|
||||
}
|
||||
/* Run member sync, but not if the only change is the last_activity timestamp */
|
||||
if( \count( $this->changedCustomFields ) > 0 OR ( \count( $changes ) > 0 AND !( \count( $changes ) === 1 AND isset( $changes['last_activity'] ) ) ) )
|
||||
{
|
||||
@@ -784,6 +842,12 @@ class _Member extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
$this->members_bitoptions['validating'] = FALSE;
|
||||
}
|
||||
|
||||
/* Revoke oAuth tokens */
|
||||
if( $value )
|
||||
{
|
||||
\IPS\Db::i()->update( 'core_oauth_server_access_tokens', array( 'status' => 'revoked' ), array( 'member_id=?', $this->member_id ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2011,7 +2075,14 @@ class _Member extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
return $this->_lang;
|
||||
}
|
||||
|
||||
|
||||
/* If in API use the lang set by dispatcher */
|
||||
if ( \IPS\Dispatcher::hasInstance() and class_exists( 'IPS\Dispatcher', FALSE ) AND \IPS\Dispatcher::i()->controllerLocation === 'api' )
|
||||
{
|
||||
$this->_lang = \IPS\Dispatcher::i()->_setLanguage();
|
||||
return $this->_lang;
|
||||
}
|
||||
|
||||
/* If in setup, create a "dummy" language */
|
||||
if ( \IPS\Dispatcher::hasInstance() and class_exists( 'IPS\Dispatcher', FALSE ) AND \IPS\Dispatcher::i()->controllerLocation === 'setup' AND \IPS\Dispatcher::i()->setupLocation === 'install' )
|
||||
{
|
||||
@@ -3418,7 +3489,6 @@ class _Member extends \IPS\Patterns\ActiveRecord
|
||||
* @apiresponse string title Member title
|
||||
* @clientapiresponse string timezone Member timezone
|
||||
* @apiresponse string formattedName Username with group formatting
|
||||
* @apiresponse string ipAddress IP address used during registration
|
||||
* @apiresponse \IPS\Member\Group primaryGroup Primary group
|
||||
* @clientapiresponse [\IPS\Member\Group] secondaryGroups Secondary groups
|
||||
* @clientapiresponse string email Email address
|
||||
@@ -3445,7 +3515,17 @@ class _Member extends \IPS\Patterns\ActiveRecord
|
||||
*/
|
||||
public function apiOutput( \IPS\Member $authorizedMember = NULL, $otherFields = NULL )
|
||||
{
|
||||
$group = \IPS\Member\Group::load( $this->_data['member_group_id'] );
|
||||
try
|
||||
{
|
||||
$group = \IPS\Member\Group::load( $this->_data['member_group_id'] );
|
||||
}
|
||||
catch( \OutOfRangeException $e )
|
||||
{
|
||||
\IPS\Log::log( "{$this->name} has an invalid group ({$this->_data['member_group_id']}) during API call. Resetting.", 'api_invalid_group' );
|
||||
$group = \IPS\Member\Group::load( \IPS\Settings::i()->member_group ); // Intentially no catch here as that means things are really broken.
|
||||
$this->member_group_id = \IPS\Settings::i()->member_group;
|
||||
$this->save();
|
||||
}
|
||||
|
||||
$secondaryGroups = array();
|
||||
foreach ( array_filter( array_map( "intval", explode( ',', $this->_data['mgroup_others'] ) ) ) as $secondaryGroupId )
|
||||
@@ -3471,7 +3551,8 @@ class _Member extends \IPS\Patterns\ActiveRecord
|
||||
|
||||
foreach( $profileFields as $field )
|
||||
{
|
||||
if ( !$authorizedMember or
|
||||
if ( !$authorizedMember or
|
||||
$field['pf_contains_pii'] == 1 AND ( $authorizedMember->member_id == $this->member_id ) or
|
||||
$field['pf_member_hide'] == 'all' or
|
||||
( $field['pf_member_hide'] == 'owner' and ( $authorizedMember->member_id == $this->member_id OR $authorizedMember->modPermissions() OR $authorizedMember->isAdmin() ) ) or
|
||||
( $field['pf_member_hide'] == 'staff' and ( $authorizedMember->modPermissions() OR $authorizedMember->isAdmin() ) )
|
||||
@@ -3914,9 +3995,11 @@ class _Member extends \IPS\Patterns\ActiveRecord
|
||||
* @param string $type Request type
|
||||
* @param string $emailAddress Email address to check, NULL for existing email address
|
||||
* @param int $spamCode If set, will modify by reference with the raw value from the spam service
|
||||
* @param bool $disposable If set, will modify by reference if the user used a disposable email.
|
||||
* @param bool $geoBlock If set, will modify by reference if the user is in a moderated / blocked country.
|
||||
* @return int|NULL Action code based on spam service response, or NULL for no action
|
||||
*/
|
||||
public function spamService( $type='register', $emailAddress=NULL, &$spamCode=NULL )
|
||||
public function spamService( $type='register', $emailAddress=NULL, &$spamCode=NULL, &$disposable=FALSE, &$geoBlock=FALSE )
|
||||
{
|
||||
$email = $emailAddress ?: $this->email;
|
||||
|
||||
@@ -4004,10 +4087,156 @@ class _Member extends \IPS\Patterns\ActiveRecord
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* GeoLocation */
|
||||
if ( $type === 'register' AND $action == 1 )
|
||||
{
|
||||
$action = $this->geoSpamCheck( $geoBlock );
|
||||
}
|
||||
|
||||
/* If the normal spam service and geolocation didn't pick up anything, check for a disposable email. */
|
||||
if ( $type === 'register' AND $action == 1 )
|
||||
{
|
||||
$action = $this->disposableEmailCheck( $emailAddress, $disposable );
|
||||
}
|
||||
|
||||
return $action;
|
||||
}
|
||||
|
||||
/**
|
||||
* GeoLocation Block Check
|
||||
*
|
||||
* @param bool $geoBlock If set, will modify by reference if the user is in a moderated / blocked country.
|
||||
* @return int|NULL
|
||||
*/
|
||||
public function geoSpamCheck( &$geoBlock ): ?int
|
||||
{
|
||||
/* If Geo is enabled, don't bother. */
|
||||
if ( !\IPS\Settings::i()->ipsgeoip )
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* See we have anything configured. If we don't, don't bother. */
|
||||
$settings = \IPS\Settings::i()->spam_geo_settings ? json_decode( \IPS\Settings::i()->spam_geo_settings, true ) : array();
|
||||
|
||||
if ( !\count( $settings ) )
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$location = \IPS\GeoLocation::getRequesterLocation();
|
||||
}
|
||||
catch( \BadMethodCallException | \IPS\Http\Request\Exception | \RuntimeException | \OutOfRangeException $e )
|
||||
{
|
||||
/* If it fails for any reason, don't bother. */
|
||||
\IPS\Log::debug( $e, 'spam-service' );
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* All good, start checking. */
|
||||
if ( !isset( $settings[ $location->country ] ) )
|
||||
{
|
||||
/* Not in the list, we're good. */
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* See what action we're taking. */
|
||||
switch( $settings[$location->country] )
|
||||
{
|
||||
case 'moderate':
|
||||
\IPS\Settings::i()->reg_auth_type = 'admin';
|
||||
$geoBlock = TRUE;
|
||||
return 2;
|
||||
|
||||
case 'block':
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for disposable email address domain
|
||||
*
|
||||
* @param string $emailAddress Email to check, or NULL for current.
|
||||
* @param bool $disposable If set, will modify by reference if the user used a disposable email.
|
||||
* @return int|NULL
|
||||
*/
|
||||
public function disposableEmailCheck( $emailAddress, &$disposable=FALSE ): ?int
|
||||
{
|
||||
$email = $emailAddress ?: $this->email;
|
||||
|
||||
try
|
||||
{
|
||||
$response = \IPS\Http\Url::ips( 'spam/disposable' )->request()->login( \IPS\Settings::i()->ipb_reg_number, '' )->post( array(
|
||||
'email' => $email,
|
||||
) );
|
||||
|
||||
if ( $response->httpResponseCode != 200 )
|
||||
{
|
||||
throw new \DomainException( print_r( $response, TRUE ) );
|
||||
}
|
||||
|
||||
$result = (bool) $response->decodeJson()['result'];
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::debug( $e, 'spam-service' );
|
||||
$result = false;
|
||||
}
|
||||
|
||||
$action = NULL;
|
||||
if ( $result )
|
||||
{
|
||||
$disposable = TRUE;
|
||||
|
||||
$action = \IPS\Settings::i()->spam_service_disposable;
|
||||
|
||||
/* Perform Action */
|
||||
switch( $action )
|
||||
{
|
||||
/* Proceed with registration */
|
||||
case 1:
|
||||
break;
|
||||
|
||||
/* Flag for admin approval */
|
||||
case 2:
|
||||
\IPS\Settings::i()->reg_auth_type = 'admin';
|
||||
break;
|
||||
|
||||
/* Approve the account, but ban it */
|
||||
case 3:
|
||||
$this->temp_ban = -1;
|
||||
$this->members_bitoptions['bw_is_spammer'] = TRUE;
|
||||
break;
|
||||
|
||||
/* Deny registration - we return the code and the controller is expected to show an error */
|
||||
case 4:
|
||||
break;
|
||||
|
||||
/* Moderate posts */
|
||||
case 5:
|
||||
$days = json_decode( \IPS\Settings::i()->spam_service_days, TRUE );
|
||||
|
||||
if ( isset( $days['disposable'] ) )
|
||||
{
|
||||
if ( $days['disposable'] == -1 )
|
||||
{
|
||||
$this->mod_posts = -1;
|
||||
}
|
||||
else if ( $days['disposable'] > 0 )
|
||||
{
|
||||
$this->mod_posts = \IPS\DateTime::ts( time() )->add( new \DateInterval( "P{$days['disposable']}D" ) )->getTimestamp();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Member Sync
|
||||
*
|
||||
@@ -4597,6 +4826,8 @@ class _Member extends \IPS\Patterns\ActiveRecord
|
||||
/* Reset the flag */
|
||||
$this->members_bitoptions['validating'] = FALSE;
|
||||
$this->save();
|
||||
|
||||
\IPS\Api\Webhook::fire( 'member_registration_complete', $this, $this->webhookFilters() );
|
||||
|
||||
/* Sync */
|
||||
$this->memberSync( 'onValidate' );
|
||||
@@ -5931,7 +6162,7 @@ class _Member extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
foreach( $members as $data )
|
||||
{
|
||||
list( $type, $memberId ) = explode( '-', $data );
|
||||
[ $type, $memberId ] = explode( '-', $data );
|
||||
$insertId = \IPS\Db::i()->insert( 'core_achievements_log_milestones', [
|
||||
'milestone_member_id' => $memberId,
|
||||
'milestone_rule' => $ruleId,
|
||||
@@ -6190,4 +6421,95 @@ class _Member extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the member's personal information
|
||||
*
|
||||
* @return \IPS\Xml\SimpleXML
|
||||
*/
|
||||
public function getPiiData(): \IPS\Xml\SimpleXML
|
||||
{
|
||||
/* Init */
|
||||
$xml = \IPS\Xml\SimpleXML::create('member_export');
|
||||
$xml->addAttribute( 'created', \IPS\DateTime::ts( time() )->rfc3339() );
|
||||
|
||||
/* Get the data */
|
||||
foreach( \IPS\Application::allExtensions( 'core', 'MemberExportPersonalInformation', TRUE, 'core' ) AS $key => $ext )
|
||||
{
|
||||
if ( $data = $ext->getData( $this ) and \is_array( $data ) and \count( $data ) )
|
||||
{
|
||||
$child = $xml->addChild( $key );
|
||||
foreach( $data as $k => $v )
|
||||
{
|
||||
$child->addChild( $k, $v );
|
||||
}
|
||||
}
|
||||
}
|
||||
return $xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is there a deletion request pending?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function get_isDeletionPending(): bool
|
||||
{
|
||||
$where = [];
|
||||
$where[] = ['member_id=?', $this->member_id];
|
||||
$where[] = [ \IPS\Db::i()->in( 'action',[\IPS\Member\PrivacyAction::TYPE_REQUEST_DELETE, \IPS\Member\PrivacyAction::TYPE_REQUEST_DELETE_VALIDATION ] ) ];
|
||||
|
||||
return !\IPS\Db::i()->select( 'count(*)', \IPS\Member\PrivacyAction::$databaseTable, $where )->first() == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all logged IP addresses which were logged after a certain time
|
||||
*
|
||||
* @param int $time
|
||||
* @return void
|
||||
*/
|
||||
public static function pruneAllLoggedIpAddresses(int $time )
|
||||
{
|
||||
foreach ( \IPS\Content::routedClasses( FALSE, TRUE ) as $class )
|
||||
{
|
||||
try
|
||||
{
|
||||
$class::pruneIpAddresses( \IPS\Settings::i()->ip_address_prune );
|
||||
}
|
||||
catch( \Exception $ex ) { }
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* PMs and Ratings are treated extra */
|
||||
\IPS\Db::i()->update( 'core_message_posts', array( 'msg_ip_address' => '' ), array( "msg_ip_address != '' AND msg_date <= " . $time ) );
|
||||
\IPS\Db::i()->update( 'core_ratings', array( 'ip' => '' ), array( "ip != '' AND rating_date IS NOT NULL AND rating_date <= " . $time ) );
|
||||
|
||||
foreach ( \IPS\Application::allExtensions( 'core', 'IpAddresses', FALSE, 'core' ) as $key => $extension )
|
||||
{
|
||||
if( method_exists( $extension, 'pruneIpAddress'))
|
||||
{
|
||||
$extension->pruneIpAddress( $time );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Can this member request that his account gets deleted via the frontend
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canUseAccountDeletion(): bool
|
||||
{
|
||||
if( \IPS\Settings::i()->right_to_be_forgotten_type != 'on' )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if( \IPS\Member::loggedIn()->isAdmin() )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Privacy Action
|
||||
* @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 07 March 2023
|
||||
*/
|
||||
|
||||
namespace IPS\Member;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* PrivacyAction Model
|
||||
*/
|
||||
class _PrivacyAction extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
/**
|
||||
* @brief [ActiveRecord] Multiton Store
|
||||
*/
|
||||
protected static $multitons;
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Database Table
|
||||
*/
|
||||
public static $databaseTable = 'core_member_privacy_actions';
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Multiton Map
|
||||
*/
|
||||
protected static $multitonMap = [];
|
||||
|
||||
/**
|
||||
* @brief PII request
|
||||
*/
|
||||
const TYPE_REQUEST_PII = 'pii_download';
|
||||
|
||||
/**
|
||||
* @brief Account deletion request
|
||||
*/
|
||||
const TYPE_REQUEST_DELETE = 'delete_account';
|
||||
|
||||
/**
|
||||
* @brief Account deletion requires validation
|
||||
*/
|
||||
const TYPE_REQUEST_DELETE_VALIDATION = 'delete_account_validation';
|
||||
|
||||
/**
|
||||
* Request the PII Data
|
||||
*
|
||||
* @param \IPS\Member|NULL $member
|
||||
* @return void
|
||||
*/
|
||||
public static function requestPiiData( \IPS\Member $member = NULL )
|
||||
{
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
$obj = new static;
|
||||
$obj->member_id = $member->member_id;
|
||||
$obj->request_date = time();
|
||||
$obj->action = static::TYPE_REQUEST_PII;
|
||||
$obj->save();
|
||||
|
||||
\IPS\core\AdminNotification::send( 'core', 'PiiDataRequest', NULL, TRUE, $member );
|
||||
}
|
||||
|
||||
/**
|
||||
* Can the member request his PII Data?
|
||||
*
|
||||
* @param \IPS\Member|NULL $member
|
||||
* @return bool
|
||||
*/
|
||||
public static function canRequestPiiData( \IPS\Member $member = NULL ): bool
|
||||
{
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
if ( $member->member_id AND !static::hasPiiRequest($member) )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can the member download his PII Data?
|
||||
*
|
||||
* @param \IPS\Member|NULL $member
|
||||
* @return bool
|
||||
*/
|
||||
public static function canDownloadPiiData( \IPS\Member $member = NULL ): bool
|
||||
{
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
if( !$member->member_id )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if ( \IPS\Db::i()->select( 'count(*)', static::$databaseTable, [ 'member_id=? AND action=? AND approved=?', $member->member_id, static::TYPE_REQUEST_PII, 1 ] )->first() > 0 )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the deletion request by member and key
|
||||
*
|
||||
* @param \IPS\Member $member
|
||||
* @param string $key
|
||||
* @return static
|
||||
* @throws \OutOfRangeException
|
||||
*/
|
||||
public static function getDeletionRequestByMemberAndKey( \IPS\Member $member, string $key ): static
|
||||
{
|
||||
try
|
||||
{
|
||||
$where = [];
|
||||
$where[] = ['member_id=?', $member->member_id];
|
||||
$where[] = ['vkey=?', $key];
|
||||
$where[] = [ \IPS\Db::i()->in( 'action',[static::TYPE_REQUEST_DELETE, static::TYPE_REQUEST_DELETE_VALIDATION ] )];
|
||||
return static::constructFromData(\IPS\Db::i()->select( '*', static::$databaseTable, $where )->first() );
|
||||
}
|
||||
catch( \UnderflowException $e ){
|
||||
throw new \OutOfRangeException;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is a PII Data Request pending for this member?
|
||||
*
|
||||
* @param \IPS\Member|NULL $member
|
||||
* @param bool $approved
|
||||
* @return bool
|
||||
*/
|
||||
public static function hasPiiRequest( \IPS\Member $member = NULL, bool $approved = FALSE ): bool
|
||||
{
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
try
|
||||
{
|
||||
\IPS\Db::i()->select( '*', static::$databaseTable, [ 'member_id=? AND action=? AND approved=?', $member->member_id, static::TYPE_REQUEST_PII, (int) $approved ] )->first();
|
||||
return TRUE;
|
||||
}
|
||||
catch( \UnderflowException $e ){}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Approve the PII Request
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function approvePiiRequest()
|
||||
{
|
||||
$this->approved = TRUE;
|
||||
$this->save();
|
||||
$notification = new \IPS\Notification( \IPS\Application::load( 'core' ), 'pii_data', $this, array( $this ) );
|
||||
$notification->recipients->attach( $this->member );
|
||||
$notification->send();
|
||||
static::resetPiiAcpNotifications();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject the PII Data Request
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function rejectPiiRequest()
|
||||
{
|
||||
$notification = new \IPS\Notification( \IPS\Application::load( 'core' ), 'pii_data_rejected', $this, array( $this ) );
|
||||
$notification->recipients->attach( $this->member );
|
||||
$notification->send();
|
||||
$this->delete();
|
||||
static::resetPiiAcpNotifications();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the member object
|
||||
*
|
||||
* @return \IPS\Member
|
||||
*/
|
||||
public function get_member(): \IPS\Member
|
||||
{
|
||||
return \IPS\Member::load( $this->member_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the PII request ACP notifications
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function resetPiiAcpNotifications()
|
||||
{
|
||||
if ( !\IPS\Db::i()->select( 'COUNT(*)', static::$databaseTable, array( 'action=? AND approved=?', static::TYPE_REQUEST_PII, 0 ) )->first() )
|
||||
{
|
||||
\IPS\core\AdminNotification::remove( 'core', 'PiiDataRequest' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the account deletion ACP notifications
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function resetDeletionAcpNotifications()
|
||||
{
|
||||
if ( !\IPS\Db::i()->select( 'COUNT(*)', static::$databaseTable, array( 'action=?', static::TYPE_REQUEST_DELETE ) )->first() )
|
||||
{
|
||||
\IPS\core\AdminNotification::remove( 'core', 'AccountDeletion' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Can the current member request account deletion?
|
||||
*
|
||||
* @param \IPS\Member|NULL $member
|
||||
* @return bool
|
||||
*/
|
||||
public static function canDeleteAccount( \IPS\Member $member = NULL ): bool
|
||||
{
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
|
||||
return \IPS\Db::i()->select( 'count(*)', static::$databaseTable, [ 'member_id=? AND action=?', $member->member_id, static::TYPE_REQUEST_DELETE ] )->first() == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and log the account deletion request
|
||||
*
|
||||
* @param \IPS\Member|NULL $member
|
||||
* @return void
|
||||
*/
|
||||
public static function requestAccountDeletion( \IPS\Member $member = NULL )
|
||||
{
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
$obj = new static;
|
||||
$obj->member_id = $member->member_id;
|
||||
$obj->request_date = time();
|
||||
$obj->action = static::TYPE_REQUEST_DELETE_VALIDATION;
|
||||
$vkey = md5( $member->members_pass_hash . \IPS\Login::generateRandomString() );
|
||||
$obj->vkey = $vkey;
|
||||
\IPS\Email::buildFromTemplate( 'core', 'account_deletion_confirmation', array( $member, $vkey ), \IPS\Email::TYPE_TRANSACTIONAL )->send( $member );
|
||||
|
||||
$obj->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm the account deletion request
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function confirmAccountDeletion()
|
||||
{
|
||||
$this->action = static::TYPE_REQUEST_DELETE;
|
||||
\IPS\core\AdminNotification::send( 'core', 'AccountDeletion', NULL, TRUE, $this->member );
|
||||
$this->member->logHistory( 'core', 'privacy', array( 'type' => 'account_deletion_requested' ) );
|
||||
$this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the account
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function deleteAccount()
|
||||
{
|
||||
/** @var \IPS\Member $member */
|
||||
$member = $this->member;
|
||||
|
||||
$member->delete( TRUE, FALSE );
|
||||
static::resetDeletionAcpNotifications();
|
||||
\IPS\Session::i()->log( 'acplog__members_deleted_id', array( $member->name => FALSE, $member->member_id => FALSE ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject the account deletion request
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function rejectDeletionRequest()
|
||||
{
|
||||
$notification = new \IPS\Notification( \IPS\Application::load( 'core' ), 'account_del_request_rejected', $this, array( $this ) );
|
||||
$notification->recipients->attach( $this->member );
|
||||
$notification->send();
|
||||
$this->delete();
|
||||
static::resetDeletionAcpNotifications();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all ACP notifications and caches if there are any, should be called after a member was deleted or merged
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function resetAcpNotificationCounts()
|
||||
{
|
||||
static::resetPiiAcpNotifications();
|
||||
static::resetDeletionAcpNotifications();
|
||||
}
|
||||
}
|
||||
@@ -90,7 +90,7 @@ class _NodeController extends \IPS\Api\Controller
|
||||
* Delete
|
||||
*
|
||||
* @param int $id ID Number
|
||||
* @param int|NULL $deleteChildrenOrMove -1 to delete all child nodes, or the new parent node ID to move the children to
|
||||
* @param int|NULL $deleteChildrenOrMove -1 to delete all child nodes and content, or the new parent node ID to move the children and content to
|
||||
* @throws 1S359/1 INVALID_ID The node ID does not exist
|
||||
* @throws 1S359/2 INVALID_TARGET The target node cannot be deleted because the new parent node does not exist
|
||||
* @throws 1S359/3 HAS_CHILDREN The target node cannot be deleted because it has children (pass deleteChildrenOrMove in the request to specify how to handle the children)
|
||||
@@ -104,7 +104,7 @@ class _NodeController extends \IPS\Api\Controller
|
||||
{
|
||||
$node = $class::load( $id );
|
||||
|
||||
if ( $node->hasChildren( NULL, NULL, TRUE ) )
|
||||
if ( $node->hasChildren( NULL, NULL, TRUE ) OR ( isset( $node::$contentItemClass ) AND $node::$contentItemClass ) )
|
||||
{
|
||||
/* -1 means delete everything */
|
||||
if ( $deleteChildrenOrMove AND $deleteChildrenOrMove == -1 )
|
||||
@@ -116,7 +116,7 @@ class _NodeController extends \IPS\Api\Controller
|
||||
try
|
||||
{
|
||||
$target = $class::load( $deleteChildrenOrMove );
|
||||
$node->deleteOrMoveFormSubmit( array( 'node_move_children' => TRUE, 'node_destination' => $target->_id ) );
|
||||
$node->deleteOrMoveFormSubmit( array( 'node_move_children' => TRUE, 'node_destination' => $target->_id, 'node_move_content' => $target ) );
|
||||
}
|
||||
catch ( \OutOfRangeException $e )
|
||||
{
|
||||
|
||||
@@ -1037,7 +1037,7 @@ class _Notification
|
||||
/* Check device is logged in */
|
||||
try
|
||||
{
|
||||
$device = \IPS\Member\Device::load( $auth['device'] );
|
||||
$device = \IPS\Member\Device::load( $auth['device'], NULL, [ 'member_id=?', $auth['member'] ] );
|
||||
if ( !$device->login_key )
|
||||
{
|
||||
/* Is not, continue */
|
||||
|
||||
@@ -939,7 +939,13 @@ class _Output
|
||||
$output = str_replace( '<!--ipsCachingLog-->', \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->cachingLog( $cachingLog ), $output );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* VLE language bits now parseOutputForDisplay has run */
|
||||
if ( \IPS\Lang::vleActive() )
|
||||
{
|
||||
$output = str_replace( '<!--ipsVleWords-->', 'var ipsVle = ' . json_encode( \IPS\Member::loggedIn()->language()->vleForJson(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS ) . ';', $output );
|
||||
}
|
||||
|
||||
/* Check for any autosave cookies */
|
||||
if ( \count( \IPS\Request::i()->clearAutoSaveCookie ) )
|
||||
{
|
||||
|
||||
@@ -104,15 +104,17 @@ class _Hook extends \IPS\Patterns\ActiveRecord
|
||||
|
||||
/* Work out the contents */
|
||||
$contents = str_replace(
|
||||
array(
|
||||
[
|
||||
'{abstract}',
|
||||
'{classname}',
|
||||
),
|
||||
array(
|
||||
'{extended_class}',
|
||||
],
|
||||
[
|
||||
$abstract,
|
||||
$classname,
|
||||
),
|
||||
\file_get_contents( \IPS\ROOT_PATH . '/applications/core/data/defaults/Hook.txt' )
|
||||
$hook->class
|
||||
],
|
||||
\file_get_contents( \IPS\ROOT_PATH . '/applications/core/data/defaults/Hook.txt' )
|
||||
);
|
||||
|
||||
\file_put_contents( $hookDir . "/{$hook->filename}.php", $contents );
|
||||
|
||||
@@ -433,7 +433,7 @@ CODE;
|
||||
}
|
||||
|
||||
/* Upgrade */
|
||||
if( \IPS\Member::loggedIn()->hasAcpRestriction( 'core', 'applications', 'plugins_install' ) AND !$this->marketplace_id AND \IPS\IPS::canManageResources() AND \IPS\IPS::checkThirdParty() )
|
||||
if( \IPS\Member::loggedIn()->hasAcpRestriction( 'core', 'applications', 'plugins_install' ) AND \IPS\IPS::canManageResources() )
|
||||
{
|
||||
$buttons['upgrade'] = array(
|
||||
'icon' => 'upload',
|
||||
@@ -481,14 +481,11 @@ CODE;
|
||||
'link' => \IPS\Http\Url::internal( "app=core&module=applications&controller=plugins&do=developer&id={$this->_id}" ),
|
||||
);
|
||||
|
||||
if( !$this->marketplace_id )
|
||||
{
|
||||
$buttons['download'] = array(
|
||||
'icon' => 'download',
|
||||
'title' => 'download',
|
||||
'link' => \IPS\Http\Url::internal("app=core&module=applications&controller=plugins&do=download&id={$this->_id}")->csrf(),
|
||||
);
|
||||
}
|
||||
$buttons['download'] = array(
|
||||
'icon' => 'download',
|
||||
'title' => 'download',
|
||||
'link' => \IPS\Http\Url::internal("app=core&module=applications&controller=plugins&do=download&id={$this->_id}")->csrf(),
|
||||
);
|
||||
}
|
||||
|
||||
if ( file_exists( \IPS\ROOT_PATH . "/plugins/{$this->location}/{$this->name}_{$this->version_human}.xml" ) AND !\IPS\IN_DEV )
|
||||
|
||||
@@ -125,9 +125,9 @@ class _Poll extends \IPS\Patterns\ActiveRecord implements \SplSubject
|
||||
/**
|
||||
* Get Poll Close Date
|
||||
*
|
||||
* @return \IPS\DateTime
|
||||
* @return \IPS\DateTime|int
|
||||
*/
|
||||
public function get_poll_close_date()
|
||||
public function get_poll_close_date() : \IPS\DateTime|int
|
||||
{
|
||||
if( $this->_pollCloseDateObject instanceof \IPS\DateTime )
|
||||
{
|
||||
@@ -680,7 +680,7 @@ class _Poll extends \IPS\Patterns\ActiveRecord implements \SplSubject
|
||||
'title' => $this->poll_question,
|
||||
'startDate' => $this->start_date->rfc3339(),
|
||||
'closed' => (bool) $this->poll_closed,
|
||||
'closedDate' => ( $this->poll_closed ? $this->poll_close_date->rfc3339() : null ),
|
||||
'closedDate' => ( ( $this->poll_closed AND $this->poll_close_date instanceof \IPS\DateTime ) ? $this->poll_close_date->rfc3339() : null ),
|
||||
'public' => (bool) $this->poll_view_voters,
|
||||
'votes' => $this->votes,
|
||||
'questions' => $questions
|
||||
|
||||
@@ -50,7 +50,7 @@ class _Request extends \IPS\Patterns\Singleton
|
||||
$this->parseIncomingRecursively( $_GET );
|
||||
$this->parseIncomingRecursively( $_POST );
|
||||
}
|
||||
|
||||
|
||||
array_walk_recursive( $_COOKIE, array( $this, 'clean' ) );
|
||||
|
||||
/* If we have a cookie prefix, we have to strip it first */
|
||||
@@ -380,6 +380,16 @@ class _Request extends \IPS\Patterns\Singleton
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get essential cookies
|
||||
*
|
||||
* @return array|string[] List of essential cookies that can't be skipped
|
||||
*/
|
||||
public static function getEssentialCookies(): array
|
||||
{
|
||||
return \IPS\Application::getEssentialCookieNames();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a cookie
|
||||
@@ -394,6 +404,13 @@ class _Request extends \IPS\Patterns\Singleton
|
||||
*/
|
||||
public function setCookie( $name, $value, $expire=NULL, $httpOnly=TRUE, $domain=NULL, $path=NULL )
|
||||
{
|
||||
/* Let's see if this is an optional cookie and if it is one, if the user wants them */
|
||||
if( $this->skipCookie( $name ) )
|
||||
{
|
||||
\IPS\Log::debug('skipping ' . $name, 'cookie' );
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Work out the path and if cookies should be SSL only */
|
||||
$sslOnly = FALSE;
|
||||
if( mb_substr( \IPS\Settings::i()->base_url, 0, 5 ) == 'https' AND \IPS\COOKIE_BYPASS_SSLONLY !== TRUE )
|
||||
@@ -425,6 +442,61 @@ class _Request extends \IPS\Patterns\Singleton
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should the cookie be set or skipped? Takes the controller location and members cookie preferences into account.
|
||||
*
|
||||
* @param string $name
|
||||
* @return bool
|
||||
*/
|
||||
protected function skipCookie( string $name ): bool
|
||||
{
|
||||
if( \IPS\Dispatcher::hasInstance() AND \IPS\Dispatcher::i()->controllerLocation === 'admin' )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
elseif( $this->cookieConsentEnabled() !== TRUE )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if( in_array( $name, static::getEssentialCookies() ) )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Check wildcard cookies */
|
||||
foreach( static::getEssentialCookies() as $c )
|
||||
{
|
||||
if( mb_substr( $c, -1, 1 ) === '*' AND rtrim( $name, '*' ) === $name )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
return ( !\IPS\Member::loggedIn()->optionalCookiesAllowed );
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Storage of cookie consent status
|
||||
*/
|
||||
protected $_cookieConsentEnabled = NULL;
|
||||
|
||||
/**
|
||||
* Check whether cookie consent is required
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function cookieConsentEnabled():? bool
|
||||
{
|
||||
// See if cookie consent is enabled
|
||||
if( \IPS\Dispatcher::hasInstance() AND $this->_cookieConsentEnabled === NULL )
|
||||
{
|
||||
$this->_cookieConsentEnabled = ( \IPS\Settings::i()->guest_terms_bar AND mb_strstr( \IPS\Member::loggedIn()->language()->get('guest_terms_bar_text_value'), '%4$s' ) );
|
||||
}
|
||||
|
||||
return $this->_cookieConsentEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear login cookies
|
||||
|
||||
@@ -214,12 +214,12 @@ class _Front extends \IPS\Session
|
||||
{
|
||||
/* Authenticate */
|
||||
$device = \IPS\Member\Device::loadAndAuthenticate( \IPS\Request::i()->cookie['device_key'], $member, \IPS\Request::i()->cookie['login_key'] );
|
||||
|
||||
/* Set member in session */
|
||||
$this->member = $member;
|
||||
|
||||
/* Refresh the device key cookie */
|
||||
\IPS\Request::i()->setCookie( 'device_key', \IPS\Request::i()->cookie['device_key'], ( new \IPS\DateTime )->add( new \DateInterval( 'P1Y' ) ) );
|
||||
|
||||
/* Set member in session */
|
||||
$this->member = $member;
|
||||
|
||||
$member->recordLogin();
|
||||
$member->achievementAction( 'core', 'SessionStartDaily' );
|
||||
|
||||
+10
-4
@@ -26,7 +26,7 @@ class _Parser
|
||||
* @brief Regex for detecting an Emoji character
|
||||
* @note This string is automatically generated by the Emoji Data Builder. Do not modify it manually.
|
||||
*/
|
||||
const EMOJI_REGEX = '\x{1F468}\x{200D}\x{2764}\x{FE0F}\x{200D}\x{1F48B}\x{200D}\x{1F468}|\x{1F469}\x{200D}\x{2764}\x{FE0F}\x{200D}\x{1F48B}\x{200D}\x{1F468}|\x{1F469}\x{200D}\x{2764}\x{FE0F}\x{200D}\x{1F48B}\x{200D}\x{1F469}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F91D}\x{200D}\x{1F9D1}|\x{1F3F4}\x{E0067}\x{E0062}\x{E0065}\x{E006E}\x{E0067}\x{E007F}|\x{1F3F4}\x{E0067}\x{E0062}\x{E0077}\x{E006C}\x{E0073}\x{E007F}|\x{1F3F4}\x{E0067}\x{E0062}\x{E0073}\x{E0063}\x{E0074}\x{E007F}|\x{1F468}\x{200D}\x{1F469}\x{200D}\x{1F467}\x{200D}\x{1F466}|\x{1F468}\x{200D}\x{1F469}\x{200D}\x{1F466}\x{200D}\x{1F466}|\x{1F468}\x{200D}\x{1F468}\x{200D}\x{1F467}\x{200D}\x{1F467}|\x{1F468}\x{200D}\x{1F468}\x{200D}\x{1F467}\x{200D}\x{1F466}|\x{1F468}\x{200D}\x{1F468}\x{200D}\x{1F466}\x{200D}\x{1F466}|\x{1F469}\x{200D}\x{1F469}\x{200D}\x{1F466}\x{200D}\x{1F466}|\x{1F469}\x{200D}\x{1F469}\x{200D}\x{1F467}\x{200D}\x{1F466}|\x{1F469}\x{200D}\x{1F469}\x{200D}\x{1F467}\x{200D}\x{1F467}|\x{1F468}\x{200D}\x{1F469}\x{200D}\x{1F467}\x{200D}\x{1F467}|\x{1F3CA}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2696}\x{FE0F}|\x{1F64D}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F64D}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F64B}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F64B}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F647}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F647}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F646}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F646}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2695}\x{FE0F}|\x{1F645}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F645}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9B9}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F3C3}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F3C3}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F64E}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2708}\x{FE0F}|\x{1F9D6}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9D6}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9D7}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9D7}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9D8}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F575}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F575}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9B9}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F93D}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F93D}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F93E}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F93E}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9B8}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F64E}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F6A3}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9CD}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9CF}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F3CB}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F3CB}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F3CC}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F3CC}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F939}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F938}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F938}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9CE}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F937}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F937}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9CE}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F935}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F935}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9CF}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F6A3}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F926}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F926}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F3CA}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F939}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9CD}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F6B6}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F6B6}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2695}\x{FE0F}|\x{1F6B5}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F3C4}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F3C4}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F6B5}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F6B4}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F6B4}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9B8}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9D8}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2696}\x{FE0F}|\x{1F9DB}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9DA}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2708}\x{FE0F}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2696Line truncated
|
||||
const EMOJI_REGEX = '\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2764}\x{FE0F}\x{200D}\x{1F48B}\x{200D}\x{1F468}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2764}\x{FE0F}\x{200D}\x{1F48B}\x{200D}\x{1F468}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2764}\x{FE0F}\x{200D}\x{1F48B}\x{200D}\x{1F469}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2764}\x{FE0F}\x{200D}\x{1F468}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2764}\x{FE0F}\x{200D}\x{1F468}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2764}\x{FE0F}\x{200D}\x{1F469}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F91D}\x{200D}\x{1F9D1}|\x{1F3F4}\x{E0067}\x{E0062}\x{E0065}\x{E006E}\x{E0067}\x{E007F}|\x{1F3F4}\x{E0067}\x{E0062}\x{E0073}\x{E0063}\x{E0074}\x{E007F}|\x{1F3F4}\x{E0067}\x{E0062}\x{E0077}\x{E006C}\x{E0073}\x{E007F}|\x{1F468}\x{200D}\x{1F468}\x{200D}\x{1F466}\x{200D}\x{1F466}|\x{1F468}\x{200D}\x{1F468}\x{200D}\x{1F467}\x{200D}\x{1F466}|\x{1F468}\x{200D}\x{1F468}\x{200D}\x{1F467}\x{200D}\x{1F467}|\x{1F468}\x{200D}\x{1F469}\x{200D}\x{1F466}\x{200D}\x{1F466}|\x{1F468}\x{200D}\x{1F469}\x{200D}\x{1F467}\x{200D}\x{1F466}|\x{1F468}\x{200D}\x{1F469}\x{200D}\x{1F467}\x{200D}\x{1F467}|\x{1F469}\x{200D}\x{1F469}\x{200D}\x{1F466}\x{200D}\x{1F466}|\x{1F469}\x{200D}\x{1F469}\x{200D}\x{1F467}\x{200D}\x{1F466}|\x{1F469}\x{200D}\x{1F469}\x{200D}\x{1F467}\x{200D}\x{1F467}|\x{1F3C3}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F3C3}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F3C4}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F3C4}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F3CA}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F3CA}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F3CB}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F3CB}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F3CC}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F3CC}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2695}\x{FE0F}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2696}\x{FE0F}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2708}\x{FE0F}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2695}\x{FE0F}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2696}\x{FE0F}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2708}\x{FE0F}|\x{1F46E}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F46E}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F470}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F470}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F471}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F471}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F473}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F473}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F477}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F477}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F481}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F481}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F482}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F482}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F486}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F486}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F487}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F487}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F575}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F575}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F645}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F645}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F646}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F646}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F647}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F647}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F64B}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F64B}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F64D}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F64D}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F64E}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F64E}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F6A3}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F6A3}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F6B4}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F6B4}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F6B5}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F6B5}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F6B6}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F6B6}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F926}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F926}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F935}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F935}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F937}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F937}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F938}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F938}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F939}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F939}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\xLine truncated
|
||||
|
||||
/**
|
||||
* @brief Regex for detecting email addresses
|
||||
@@ -573,6 +573,10 @@ class _Parser
|
||||
login form), and general annoyances */
|
||||
$config->set( 'Attr.AllowedClasses', static::getAllowedCssClasses() );
|
||||
|
||||
/* Increase default image width */
|
||||
$config->set( 'CSS.MaxImgLength', "4800px" );
|
||||
$config->set( 'HTML.MaxImgLength', "4800" );
|
||||
|
||||
/* Callback */
|
||||
if ( $callback )
|
||||
{
|
||||
@@ -785,16 +789,16 @@ class _Parser
|
||||
site.com/index.php?/topic/1-test/?do=embed
|
||||
site.com/index.php?app=forums&module=forums&controller=topic&id=1&do=embed
|
||||
Not Allowed: Anything which goes to anything in an /interface directory - e.g.:
|
||||
site.com/core/interface/file/attachment.php - this would automatically cause files to be downloaded
|
||||
site.com/applications/core/interface/file/attachment.php - this would automatically cause files to be downloaded
|
||||
Not Allowed: Any file traversal to expose FileSystem directories, e.g:
|
||||
site.com/core/../uploads/monthly_xx_xx/file.js?
|
||||
site.com/applications/core/../uploads/monthly_xx_xx/file.js?
|
||||
Not Allowed: URLs to the open proxy:
|
||||
site.com/index.php?app=core&module=system&controller=redirect
|
||||
*/
|
||||
$notAllowed = array();
|
||||
foreach( \IPS\Application::enabledApplications() as $app )
|
||||
{
|
||||
$notAllowed[] = str_replace( '/', '(?:/{1,})', '(?:/{0,})' . preg_quote( $app->directory . '/interface/', '%' ) );
|
||||
$notAllowed[] = str_replace( '/', '(?:/{1,})', '(?:/{0,})' . preg_quote( 'applications/' . $app->directory . '/interface/', '%' ) );
|
||||
}
|
||||
|
||||
foreach( \IPS\File::getStore() as $configuration )
|
||||
@@ -3253,6 +3257,8 @@ class _Parser
|
||||
'collegehumor.com' => array( 'http://www.collegehumor.com/oembed.json', static::EMBED_VIDEO ),
|
||||
'twitter.com' => array( 'https://publish.twitter.com/oembed', static::EMBED_TWEET ),
|
||||
'mobile.twitter.com' => array( 'https://publish.twitter.com/oembed', static::EMBED_TWEET ),
|
||||
'x.com' => array( 'https://publish.x.com/oembed', static::EMBED_TWEET ),
|
||||
'x.twitter.com' => array( 'https://publish.x.com/oembed', static::EMBED_TWEET ),
|
||||
'soundcloud.com' => array( 'https://soundcloud.com/oembed', static::EMBED_VIDEO ),
|
||||
'open.spotify.com' => array( 'https://embed.spotify.com/oembed', static::EMBED_VIDEO ),
|
||||
'play.spotify.com' => array( 'https://embed.spotify.com/oembed', static::EMBED_VIDEO ),
|
||||
|
||||
@@ -1955,15 +1955,12 @@ class _Theme extends \IPS\Node\Model
|
||||
|
||||
if ( \IPS\Member::loggedIn()->hasAcpRestriction( 'core', 'customization', 'theme_download_upload' ) AND !\IPS\DEMO_MODE )
|
||||
{
|
||||
if( !$this->marketplace_id )
|
||||
{
|
||||
$buttons['upload'] = array(
|
||||
'icon' => 'upload',
|
||||
'title' => 'theme_set_import',
|
||||
'link' => \IPS\Http\Url::internal( "app=core&module=customization&controller=themes&do=importForm&id={$this->_id}" ),
|
||||
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack( 'theme_set_import_title', FALSE, array( 'sprintf' => array( $this->_title ) ) ) )
|
||||
);
|
||||
}
|
||||
$buttons['upload'] = array(
|
||||
'icon' => 'upload',
|
||||
'title' => 'theme_set_import',
|
||||
'link' => \IPS\Http\Url::internal( "app=core&module=customization&controller=themes&do=importForm&id={$this->_id}" ),
|
||||
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack( 'theme_set_import_title', FALSE, array( 'sprintf' => array( $this->_title ) ) ) )
|
||||
);
|
||||
|
||||
$buttons['download'] = array(
|
||||
'icon' => 'download',
|
||||
|
||||
Reference in new issue
Block a user