Version 5.0.13
This commit is contained in:
1 parent
b89858b2ff
commit
5efe744c5f
1407 files changed
+69690
-30177
No files matched your search
@@ -12,6 +12,7 @@ namespace IPS\Helpers\Chart;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
|
||||
use DateInterval;
|
||||
use DateTimeZone;
|
||||
use Exception;
|
||||
use IPS\DateTime;
|
||||
@@ -153,8 +154,36 @@ class Callback extends Dynamic
|
||||
{
|
||||
$result[ $column ] = $row[ $column ];
|
||||
}
|
||||
|
||||
$data[ $row['time'] ] = $result;
|
||||
|
||||
|
||||
$time = $row['time'];
|
||||
|
||||
// UNIX timestamps are sometimes passed in. In this case, we try to make sure the timestamp format matches the defaults added via $this->initData()
|
||||
if ( is_int( $time ) )
|
||||
{
|
||||
$date = DateTime::ts( $time );
|
||||
switch ( $this->timescale )
|
||||
{
|
||||
case 'hourly':
|
||||
$time = $date->format( 'Y-n-j-h-i-s' );
|
||||
break;
|
||||
|
||||
case 'daily':
|
||||
$time = $date->format( 'Y-n-j' );
|
||||
break;
|
||||
|
||||
case 'weekly':
|
||||
/* o is the ISO year number, which we need when years roll over.
|
||||
@see http://php.net/manual/en/function.date.php#106974 */
|
||||
$time = $date->format( 'o-W' );
|
||||
break;
|
||||
|
||||
case 'monthly':
|
||||
$time = $date->format( 'Y-n' );
|
||||
break;
|
||||
}
|
||||
}
|
||||
$data[ $time ] = $result;
|
||||
}
|
||||
|
||||
ksort( $data, SORT_NATURAL );
|
||||
|
||||
@@ -230,7 +230,10 @@ class Database extends Dynamic
|
||||
$this->groupBy = $this->customFiltersForm['groupBy'];
|
||||
|
||||
$whereFunctionResult = $whereFunction( $this->savedCustomFilters );
|
||||
$this->where[] = is_array( $whereFunctionResult ) ? $whereFunctionResult : array( $whereFunctionResult );
|
||||
if( !empty( $whereFunctionResult ) )
|
||||
{
|
||||
$this->where[] = is_array( $whereFunctionResult ) ? $whereFunctionResult : array( $whereFunctionResult );
|
||||
}
|
||||
|
||||
foreach( $seriesFunction( $this->savedCustomFilters ) as $series )
|
||||
{
|
||||
@@ -379,7 +382,7 @@ class Database extends Dynamic
|
||||
|
||||
foreach ( $stmt as $k => $v )
|
||||
{
|
||||
if( count( $this->availableFilters ) and !in_array( $k, $this->currentFilters ) )
|
||||
if( !in_array( $k, $this->currentFilters ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,9 @@ use IPS\Db;
|
||||
use IPS\Helpers\Chart;
|
||||
use IPS\Helpers\Form;
|
||||
use IPS\Helpers\Form\Date;
|
||||
use IPS\Helpers\Form\Select;
|
||||
use IPS\Helpers\Form\Text;
|
||||
use IPS\Helpers\Form\YesNo;
|
||||
use IPS\Http\Url;
|
||||
use IPS\IPS;
|
||||
use IPS\Member;
|
||||
@@ -30,12 +32,19 @@ use IPS\Request;
|
||||
use IPS\Session;
|
||||
use IPS\Theme;
|
||||
use Throwable;
|
||||
use UnderflowException;
|
||||
use function array_merge;
|
||||
use function defined;
|
||||
use function explode;
|
||||
use function file_get_contents;
|
||||
use function in_array;
|
||||
use function is_array;
|
||||
use function is_numeric;
|
||||
use function is_string;
|
||||
use function iterator_to_array;
|
||||
use function json_encode;
|
||||
use function mb_substr;
|
||||
use function var_export;
|
||||
use const IPS\TEMP_DIRECTORY;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
@@ -183,6 +192,13 @@ abstract class Dynamic extends Chart
|
||||
* @brief Prefix passed to tables
|
||||
*/
|
||||
public string $tableLangPrefix = '';
|
||||
|
||||
/**
|
||||
* When downloading chart collections from the saved reports page, this indicates the type that the chart should load as by default
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public static null|string $mergedChartType = null;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
@@ -200,12 +216,38 @@ abstract class Dynamic extends Chart
|
||||
*/
|
||||
public function __construct( Url $url, string $title='', array $options=array(), string $defaultType='AreaChart', string $defaultTimescale='monthly', array $defaultTimes=array( 'start' => 0, 'end' => 0 ), string $identifier='', DateTime $minimumDate=null )
|
||||
{
|
||||
|
||||
if ( isset( static::$mergedChartType ) )
|
||||
{
|
||||
$defaultType = static::$mergedChartType;
|
||||
}
|
||||
|
||||
/* If we are deleting a chart, just do that now and redirect */
|
||||
if( isset( Request::i()->deleteChart ) )
|
||||
{
|
||||
Session::i()->csrfCheck();
|
||||
Db::i()->delete( 'core_saved_charts', array( 'chart_id=? and chart_member=?', Request::i()->deleteChart, Member::loggedIn()->member_id ) );
|
||||
Output::i()->redirect( Request::i()->url()->stripQueryString( array( 'deleteChart', 'chartId', 'csrfKey' ) ), 'chart_deleted' );
|
||||
$where = array( 'chart_id=? and (chart_member IS NULL OR chart_member=0 OR chart_member=?)', Request::i()->deleteChart, Member::loggedIn()->member_id );
|
||||
try
|
||||
{
|
||||
$reportID = Db::i()->select( "chart_report_id", "core_saved_charts", $where )->first();
|
||||
}
|
||||
catch ( UnderflowException )
|
||||
{
|
||||
$reportID = null;
|
||||
}
|
||||
Db::i()->delete( 'core_saved_charts', $where );
|
||||
$url = Request::i()->url()->stripQueryString( array( 'deleteChart', 'chartId', 'csrfKey', 'wasConfirmed' ) );
|
||||
if ( isset( $reportID ) )
|
||||
{
|
||||
$url = $url->setQueryString( "report_id", $reportID );
|
||||
}
|
||||
|
||||
if ( Request::i()->do === 'getChart' )
|
||||
{
|
||||
$url = $url->stripQueryString([ 'do' ]);
|
||||
}
|
||||
|
||||
Output::i()->redirect( $url, 'chart_deleted' );
|
||||
}
|
||||
|
||||
if ( !isset( $options['chartArea'] ) )
|
||||
@@ -351,7 +393,7 @@ abstract class Dynamic extends Chart
|
||||
}
|
||||
|
||||
/* If we have requested a saved chart, load its filters */
|
||||
if( isset( Request::i()->chartId ) AND Request::i()->chartId != '_default' AND !isset( Request::i()->filters ) )
|
||||
if( isset( Request::i()->chartId ) AND is_numeric( Request::i()->chartId ) AND !isset( Request::i()->filters ) )
|
||||
{
|
||||
foreach( $this->loadAvailableChartTabs() as $chart )
|
||||
{
|
||||
@@ -414,7 +456,7 @@ abstract class Dynamic extends Chart
|
||||
try
|
||||
{
|
||||
/* Generate a form so we can save our filters as a new saved chart */
|
||||
$this->form = new Form;
|
||||
$this->form = new Form( 'form_' . $this->identifier );
|
||||
$this->form->class = 'ipsForm--vertical ipsForm--chart-custom-filters';
|
||||
|
||||
if ( $this->customFiltersForm )
|
||||
@@ -429,18 +471,18 @@ abstract class Dynamic extends Chart
|
||||
}
|
||||
|
||||
/* 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 ( Request::i()->_nodeSelectName )
|
||||
if ( isset( Request::i()->_nodeSelectName ) and Request::i()->_nodeSelectName )
|
||||
{
|
||||
Request::i()->_nodeSelectName = $this->identifier . Request::i()->_nodeSelectName;
|
||||
return (string) $this->getCustomFiltersForm();
|
||||
}
|
||||
|
||||
/* We have an existing chart ID */
|
||||
if( isset( Request::i()->chartId ) AND Request::i()->chartId != '_default' )
|
||||
if( isset( Request::i()->chartId ) AND is_numeric( Request::i()->chartId ) )
|
||||
{
|
||||
$title = '';
|
||||
|
||||
foreach( $this->loadAvailableChartTabs() as $chart )
|
||||
foreach( $this->loadSavedCharts() as $chart )
|
||||
{
|
||||
if( $chart['chart_id'] == Request::i()->chartId )
|
||||
{
|
||||
@@ -451,7 +493,7 @@ abstract class Dynamic extends Chart
|
||||
|
||||
$custom = array();
|
||||
$chartFilters = ( isset( Request::i()->chartFilters ) and Request::i()->chartFilters ) ? Request::i()->chartFilters : array();
|
||||
$this->timescale = ( isset( Request::i()->timescale ) and Request::i()->timescale[ $this->identifier ] ) ? Request::i()->timescale[ $this->identifier ] : $chart['chart_timescale'] ?? 'monthly';
|
||||
$this->timescale = ( isset( Request::i()->timescale ) and in_array( Request::i()->timescale[ $this->identifier ], ['hourly', 'daily', 'monthly', 'weekly'] ) ) ? Request::i()->timescale[ $this->identifier ] : $chart['chart_timescale'] ?? 'monthly';
|
||||
|
||||
|
||||
foreach( Request::i() as $key => $value )
|
||||
@@ -483,7 +525,45 @@ abstract class Dynamic extends Chart
|
||||
/* We are not viewing a saved chart */
|
||||
else
|
||||
{
|
||||
$this->form->add( new Text( 'custom_chart_title', NULL, TRUE ) );
|
||||
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'admin_stats.js', 'core' ) );
|
||||
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, Theme::i()->css( 'system/statistics.css', 'core', 'admin' ) );
|
||||
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, Theme::i()->css( 'system/reports.css', 'core', 'admin' ) );
|
||||
|
||||
$reports = iterator_to_array( Db::i()->select( "*", "core_reports", order: "report_title ASC" ) );
|
||||
$reportOptions = [];
|
||||
$defaultOption = false;
|
||||
foreach( $reports as $row )
|
||||
{
|
||||
$defaultOption = $defaultOption ?: "report_" . $row["id"];
|
||||
$reportOptions["report_" . $row['id']] = $row['report_title'];
|
||||
}
|
||||
|
||||
$noTabs = (bool) $this->extension?->noTabs;
|
||||
$this->form->add( new YesNo( "statsreports_show_all_members", $noTabs, false, ['disabled' => $noTabs] ) );
|
||||
$globalTogglesOn = ['statsreports_report_select'];
|
||||
if ( empty( $reports ) )
|
||||
{
|
||||
$globalTogglesOn[] = "statsreports_report_new_title";
|
||||
}
|
||||
|
||||
$this->form->add( new YesNo( "statsreports_include_in_report", false, false, [ "togglesOn" => $globalTogglesOn ] ) );
|
||||
|
||||
// if there are saved reports, allow user to select one of them
|
||||
if ( !empty( $reportOptions ) )
|
||||
{
|
||||
// todo an autocomplete field is probably a better UX when there could be dozens of reports saved
|
||||
$this->form->add( new Select( "statsreports_report", $defaultOption, false, [
|
||||
"unlimited" => false,
|
||||
"unlimitedLang" => "statsreports_report_new",
|
||||
"unlimitedToggles" => [
|
||||
'statsreports_report_new_title'
|
||||
],
|
||||
"options" => $reportOptions,
|
||||
], id: 'statsreports_report_select' ) );
|
||||
}
|
||||
|
||||
$this->form->add( new Text( "statsreports_report_new_title", $this->title, null, id: "statsreports_report_new_title" ) );
|
||||
$this->form->add( new Text( 'custom_chart_title', $this->title, true ) );
|
||||
|
||||
if( $values = $this->form->values() )
|
||||
{
|
||||
@@ -498,14 +578,37 @@ abstract class Dynamic extends Chart
|
||||
|
||||
$chartFilters = ( isset( Request::i()->chartFilters ) and Request::i()->chartFilters ) ? Request::i()->chartFilters : array();
|
||||
|
||||
/* Store the new chart */
|
||||
$id = Db::i()->insert( 'core_saved_charts', array(
|
||||
'chart_member' => Member::loggedIn()->member_id,
|
||||
'chart_controller' => Request::i()->app . '_' . Request::i()->module . '_' . Request::i()->controller . ( Request::i()->tab ? '_' . Request::i()->tab : '' ),
|
||||
// is it saved to a report?
|
||||
$reportID = null;
|
||||
if ( @$values['statsreports_include_in_report'] )
|
||||
{
|
||||
if ( isset( $values['statsreports_report'] ) and $values['statsreports_report'] )
|
||||
{
|
||||
$reportID = (int) explode( '_', $values['statsreports_report'], 2 )[1];
|
||||
}
|
||||
else
|
||||
{
|
||||
$title = $values['statsreports_report_new_title'];
|
||||
$reportID = Db::i()->insert( "core_reports", [ "report_title" => $title] );
|
||||
}
|
||||
}
|
||||
|
||||
if ( $noTabs )
|
||||
{
|
||||
$values['statsreports_show_all_members'] = true;
|
||||
}
|
||||
|
||||
$data = array(
|
||||
'chart_member' => @$values['statsreports_show_all_members'] ? 0 : Member::loggedIn()->member_id, // we make the member "0" when the show to all members option is enabled
|
||||
'chart_controller' => $this->getController(),
|
||||
'chart_configuration' => json_encode( array_merge( $chartFilters, $custom ) ),
|
||||
'chart_timescale' => Request::i()->timescale ?? $this->timescale ?? 'monthly',
|
||||
'chart_title' => $values['custom_chart_title'],
|
||||
) );
|
||||
'chart_report_id' => $reportID
|
||||
);
|
||||
|
||||
/* Store the new chart */
|
||||
$id = Db::i()->insert( 'core_saved_charts', $data );
|
||||
|
||||
/* Set some input parameters */
|
||||
$this->url = $this->url->setQueryString( 'chartId', $id );
|
||||
@@ -526,6 +629,7 @@ abstract class Dynamic extends Chart
|
||||
|
||||
/* And then return the output we need */
|
||||
Output::i()->json( array(
|
||||
'reportURL' => ($data['chart_member'] and !$data['chart_report_id']) ? null : Url::internal( "app=core&module=overview&controller=mycharts&highlight={$id}" . ( $data['chart_report_id'] ? "&report_id={$data['chart_report_id']}" : "" ) ),
|
||||
'tabHref' => $this->url->stripQueryString( 'filters' ),
|
||||
'chartId' => $id,
|
||||
'tabId' => md5( $this->url->acpQueryString() ),
|
||||
@@ -576,6 +680,21 @@ abstract class Dynamic extends Chart
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the "controller" to use when saving this chart
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getController() : string
|
||||
{
|
||||
if ( is_string( $this->extension?->controller ) )
|
||||
{
|
||||
return $this->extension->controller;
|
||||
}
|
||||
|
||||
return Request::i()->app . '_' . Request::i()->module . '_' . Request::i()->controller . ( Request::i()->tab ? '_' . Request::i()->tab : '' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Show filter tabs
|
||||
*/
|
||||
@@ -586,6 +705,8 @@ abstract class Dynamic extends Chart
|
||||
*/
|
||||
protected ?array $availableChartTabs = NULL;
|
||||
|
||||
protected ?array $savedCharts = null;
|
||||
|
||||
/**
|
||||
* @Brief Extension
|
||||
*/
|
||||
@@ -617,19 +738,35 @@ abstract class Dynamic extends Chart
|
||||
{
|
||||
if ( $this->availableChartTabs === null )
|
||||
{
|
||||
if ( $this->extension !== null )
|
||||
if ( $this->extension?->noTabs )
|
||||
{
|
||||
$this->availableChartTabs = iterator_to_array( Db::i()->select( '*', 'core_saved_charts', ['chart_member=? AND chart_controller=?', Member::loggedIn()->member_id, $this->extension->controller] ) );
|
||||
$this->availableChartTabs = [];
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->availableChartTabs = iterator_to_array( Db::i()->select( '*', 'core_saved_charts', ['chart_member=? AND chart_controller=?', Member::loggedIn()->member_id, Request::i()->app . '_' . Request::i()->module . '_' . Request::i()->controller . ( Request::i()->tab ? '_' . Request::i()->tab : '' )] ) );
|
||||
$this->availableChartTabs = iterator_to_array( Db::i()->select( '*', 'core_saved_charts', ['chart_member=? AND chart_controller=? AND chart_report_id IS NULL', Member::loggedIn()->member_id, $this->getController()] ) );
|
||||
}
|
||||
}
|
||||
|
||||
return $this->availableChartTabs;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Load and return available chart tabs
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
protected function loadSavedCharts() : ?array
|
||||
{
|
||||
if ( $this->savedCharts === null )
|
||||
{
|
||||
$this->savedCharts = iterator_to_array( Db::i()->select( '*', 'core_saved_charts', ['(chart_member=? OR chart_member=0 OR chart_member IS NULL) AND chart_controller=?', Member::loggedIn()->member_id, $this->getController()] ) );
|
||||
}
|
||||
|
||||
return $this->savedCharts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set extension
|
||||
*
|
||||
@@ -672,6 +809,12 @@ abstract class Dynamic extends Chart
|
||||
*/
|
||||
protected function initData(): array
|
||||
{
|
||||
/* The JS can set this to "undefined" so let's make sure it's a valid value */
|
||||
if ( !in_array( $this->timescale, ["none", "hourly", "daily", "monthly", "weekly"] ) )
|
||||
{
|
||||
$this->timescale = "monthly";
|
||||
}
|
||||
|
||||
/* Init data */
|
||||
$data = array();
|
||||
if ( $this->start AND $this->timescale !== 'none' )
|
||||
|
||||
@@ -27,6 +27,8 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
|
||||
/**
|
||||
* keyCAPTCHA
|
||||
* @deprecated
|
||||
*
|
||||
*/
|
||||
class Keycaptcha implements CaptchaInterface
|
||||
{
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Cloudflare Turnstile
|
||||
* @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 13 June 2025
|
||||
*/
|
||||
|
||||
namespace IPS\Helpers\Form\Captcha;
|
||||
|
||||
use IPS\Http\Request\Exception;
|
||||
use IPS\Http\Url;
|
||||
use IPS\Log;
|
||||
use IPS\Member;
|
||||
use IPS\Platform\Bridge;
|
||||
use IPS\Request;
|
||||
use IPS\Settings;
|
||||
use IPS\Theme;
|
||||
use RuntimeException;
|
||||
use function defined;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cloudflare Turnstile
|
||||
*/
|
||||
class Turnstile implements CaptchaInterface
|
||||
{
|
||||
/**
|
||||
* Does this CAPTCHA service support being added in a modal?
|
||||
*/
|
||||
public static bool $supportsModal = TRUE;
|
||||
|
||||
/**
|
||||
* @brief Error
|
||||
*/
|
||||
protected ?string $error;
|
||||
|
||||
/**
|
||||
* Display
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getHtml(): string
|
||||
{
|
||||
return Theme::i()->getTemplate( 'forms', 'core', 'global' )->captchaTurnstile( Bridge::i()->getTurnstileCredentials()['site_key'], preg_replace( '/^(.+?)\..*$/', '$1', Member::loggedIn()->language()->short ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify
|
||||
*
|
||||
* @return bool|null TRUE/FALSE indicate if the test passed or not. NULL indicates the test failed, but the captcha system will display an error so we don't have to.
|
||||
*/
|
||||
public function verify(): ?bool
|
||||
{
|
||||
try
|
||||
{
|
||||
$response = Url::external( 'https://challenges.cloudflare.com/turnstile/v0/siteverify' )->request(5)->post( [
|
||||
'secret' => Bridge::i()->getTurnstileCredentials()['secret_key'],
|
||||
'response' => trim( Request::i()->__get('cf-turnstile-response') ),
|
||||
'remoteip' => Request::i()->ipAddress(),
|
||||
] )->decodeJson();
|
||||
|
||||
return ( ( $response['success'] ) and ( $response['hostname'] === Url::internal('')->data[ Url::COMPONENT_HOST ] ) );
|
||||
}
|
||||
catch( Exception $e )
|
||||
{
|
||||
Log::log( $e, 'turnstile_exception' );
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
catch( RuntimeException $e )
|
||||
{
|
||||
if( $e->getMessage() == 'BAD_JSON' )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1002,6 +1002,12 @@ class Editor extends FormAbstract
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Return if we have plugins already */
|
||||
if( !empty( Store::i()->editorPluginJs ) )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* If we are not in dev, check if we have the bundled JS, and if not, build it */
|
||||
$pluginJs = "";
|
||||
$hasPlugins = false;
|
||||
@@ -1063,10 +1069,8 @@ js;
|
||||
|
||||
$pluginJs = Minifier::minify( $pluginJs, array( 'flaggedComments' => false ) );
|
||||
|
||||
$jsFile = File::create( 'core_Theme', "editorPlugins.js", $pluginJs );
|
||||
Settings::i()->changeValues([
|
||||
'editor_plugin_js' => (string) $jsFile
|
||||
]);
|
||||
$jsFile = File::create( 'core_Theme', "editorPlugins.js", $pluginJs, container: 'editor', obscure: false );
|
||||
Store::i()->editorPluginJs = (string) $jsFile;
|
||||
}
|
||||
|
||||
return $hasPlugins;
|
||||
@@ -1141,6 +1145,40 @@ js;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get editor restrictions for the member based on group settings
|
||||
*
|
||||
* @param Member|null $member The member/null for logged in member
|
||||
* @param bool $comments Whether to use restrictions for the comment editor rather than the normal one
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getMemberRestrictionLevel( ?Member $member=null, bool $comments = false ) : int
|
||||
{
|
||||
$member = $member ?: Member::loggedIn();
|
||||
static $restrictLevelCache = [];
|
||||
$cacheKey = ($member->member_id ?: 0) . '-' . ((int) $comments);
|
||||
if ( !isset( $restrictLevelCache[$cacheKey] ) )
|
||||
{
|
||||
$restrictLevel = 0;
|
||||
foreach ( $member->groups as $gid )
|
||||
{
|
||||
$group = Member\Group::load( $gid );
|
||||
$restrictLevel = max( $group->getEditorRestrictionLevel( $comments ), $restrictLevel );
|
||||
|
||||
// 2 is the advanced editor, we won't find one more advanced
|
||||
if ( $restrictLevel >= 2 )
|
||||
{
|
||||
$restrictLevel = 2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$restrictLevelCache[$cacheKey] = $restrictLevel;
|
||||
}
|
||||
|
||||
return $restrictLevelCache[$cacheKey];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get editor restrictions for the member based on group settings
|
||||
*
|
||||
@@ -1153,32 +1191,24 @@ js;
|
||||
{
|
||||
$member = $member ?: Member::loggedIn();
|
||||
$settings = static::getRestrictionSetting();
|
||||
Output::i()->jsVars['editor_restrictions'] = []; // this needs to be shared to make sure the editor uses saved restriction settings of custom plugins rather than the defaults
|
||||
$restrictions = [];
|
||||
|
||||
$restrictLevel = 0;
|
||||
foreach ( $member->groups as $gid )
|
||||
{
|
||||
$group = Member\Group::load( $gid );
|
||||
$restrictLevel = max( $group->getEditorRestrictionLevel( $comments ), $restrictLevel );
|
||||
|
||||
// 2 is the advanced editor, we won't find one more advanced
|
||||
if ( $restrictLevel >= 2 )
|
||||
{
|
||||
$restrictLevel = 2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$restrictLevel = static::getMemberRestrictionLevel( $member, $comments );
|
||||
|
||||
foreach ( $settings as $level => $_restrictions )
|
||||
{
|
||||
if ( (int) $restrictLevel > (int) $level )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach( $_restrictions as $restriction )
|
||||
{
|
||||
$restrictions[] = $restriction;
|
||||
if ( preg_match( "/^ipsCustom(Node|Mark|Extension)__/", $restriction ) )
|
||||
{
|
||||
Output::i()->jsVars['editor_restrictions'][ $restriction ] = (int)$level;
|
||||
}
|
||||
|
||||
if ( $restrictLevel <= (int) $level )
|
||||
{
|
||||
$restrictions[] = $restriction;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1191,6 +1221,11 @@ js;
|
||||
}
|
||||
}
|
||||
|
||||
if ( empty( Output::i()->jsVars['editor_restrictions'] ) )
|
||||
{
|
||||
unset( Output::i()->jsVars['editor_restrictions'] );
|
||||
}
|
||||
|
||||
return $restrictions;
|
||||
}
|
||||
|
||||
@@ -1270,10 +1305,12 @@ js;
|
||||
/**
|
||||
* Get the level of a restriction based on the settings
|
||||
*
|
||||
* @param string $restriction
|
||||
* @param string $restriction The key of the restriction
|
||||
* @param ?int $default=null The default value of the restriction
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getRestrictionLevel( string $restriction ) : int
|
||||
public static function getRestrictionLevel( string $restriction, ?int $default =null ) : int
|
||||
{
|
||||
$settings = static::getRestrictionSetting();
|
||||
foreach ( $settings as $level => $restrictions )
|
||||
@@ -1291,6 +1328,6 @@ js;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
return $default ?? -1;
|
||||
}
|
||||
}
|
||||
@@ -511,7 +511,8 @@ class Form
|
||||
protected function _insert( mixed $element, string $elementKey=NULL, string $tab=NULL, string $after=NULL ) : void
|
||||
{
|
||||
$tab = $tab ?: $this->currentTab;
|
||||
|
||||
$added = false;
|
||||
|
||||
if ( $after )
|
||||
{
|
||||
$elements = array();
|
||||
@@ -521,15 +522,20 @@ class Form
|
||||
if ( $key === $after )
|
||||
{
|
||||
$elements[ $elementKey ] = $element;
|
||||
$added = true;
|
||||
}
|
||||
}
|
||||
|
||||
$this->elements[ $tab ] = $elements;
|
||||
}
|
||||
elseif( $elementKey )
|
||||
{
|
||||
$this->elements[ $tab ][ $elementKey ] = $element;
|
||||
$added = true;
|
||||
}
|
||||
else
|
||||
|
||||
/* If we haven't added the field yet, tack it on to the end */
|
||||
if( !$added )
|
||||
{
|
||||
$this->elements[ $tab ][] = $element;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@ use IPS\Db;
|
||||
use IPS\Dispatcher;
|
||||
use IPS\IPS;
|
||||
use IPS\Member;
|
||||
use IPS\Node\Grouping;
|
||||
use IPS\Node\Model;
|
||||
use IPS\Node\NodeGroup;
|
||||
use IPS\Output;
|
||||
use IPS\Patterns\ActiveRecordIterator;
|
||||
use IPS\Request;
|
||||
@@ -67,6 +69,7 @@ class Node extends FormAbstract
|
||||
'noParentNodes' => 'Custom' // If a value is provided, subnodes of this class which have no parent node will be added into a pseudo-group with the title provided. e.g. custom packages in Nexus do not belong to any package group
|
||||
'autoPopulate' => FALSE // Whether or not to autopopulate children of root nodes (defaults to TRUE which means the children are loaded, use FALSE to only show the parent nodes by default until they are clicked on)
|
||||
'clubs' => TRUE, // If TRUE, will also show nodes inside clubs that the user can access. Defaults to FALSE.
|
||||
'nodeGroups' => FALSE, // If TRUE, will show node groups for easier selection. Defaults to FALSE.
|
||||
'maxResults' => 200, // Maximum number of nodes to load. NULL to load all nodes or FALSE to respect node class definition. Defaults to FALSE.
|
||||
);
|
||||
* @endcode
|
||||
@@ -91,6 +94,7 @@ class Node extends FormAbstract
|
||||
'noParentNodes' => NULL,
|
||||
'autoPopulate' => TRUE,
|
||||
'clubs' => FALSE,
|
||||
'nodeGroups' => FALSE,
|
||||
'maxResults' => FALSE,
|
||||
);
|
||||
|
||||
@@ -121,6 +125,11 @@ class Node extends FormAbstract
|
||||
$this->options['clubs'] = FALSE;
|
||||
}
|
||||
|
||||
if( $this->options['nodeGroups'] and ! ( IPS::classUsesTrait( $this->options['class'], Grouping::class ) and Member::loggedIn()->isModerator() ) )
|
||||
{
|
||||
$this->options['nodeGroups'] = false;
|
||||
}
|
||||
|
||||
/* Do we need to respect the node class's defined max results (which is the default)? */
|
||||
if( $this->options['maxResults'] === FALSE )
|
||||
{
|
||||
@@ -237,6 +246,13 @@ class Node extends FormAbstract
|
||||
Output::i()->sendOutput( Theme::i()->getTemplate( 'forms', 'core', 'global' )->nodeCascade( $results, TRUE, $selectable, $this->options['subnodes'], $this->options['togglePerm'], $this->options['toggleIds'], $disabledCallback, FALSE, NULL, $this->options['class'], $this->options['where'], $this->options['disabled'], NULL, array(), NULL, $this->options['togglePermPBR'], $this->options['toggleIdsOff'] ) );
|
||||
}
|
||||
}
|
||||
|
||||
/* Node Groups */
|
||||
$nodeGroups = null;
|
||||
if( $this->options['nodeGroups'] )
|
||||
{
|
||||
$nodeGroups = $nodeClass::availableNodeGroups();
|
||||
}
|
||||
|
||||
/* Get initial nodes */
|
||||
$nodes = array();
|
||||
@@ -303,30 +319,68 @@ class Node extends FormAbstract
|
||||
}
|
||||
|
||||
/* What is selected? */
|
||||
if ( $this->options['zeroVal'] !== NULL and $this->value === 0 )
|
||||
if ( $this->options['zeroVal'] !== NULL and !( $this->value instanceof Model ) and !is_array( $this->value ) and $this->value == 0 )
|
||||
{
|
||||
$selected = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
$selected = array();
|
||||
$mergeNodes = array();
|
||||
|
||||
if ( is_array( $this->value ) )
|
||||
{
|
||||
/* Check to make sure we don't have a node group selected and do not have permission to select node groups */
|
||||
foreach( $this->value as $index => $node )
|
||||
{
|
||||
if ( $node instanceof NodeGroup )
|
||||
{
|
||||
if ( ! $this->options['nodeGroups'] )
|
||||
{
|
||||
/* We don't, so all we can do is get the current node IDs and present them */
|
||||
foreach( $node->nodes() as $groupedNode )
|
||||
{
|
||||
if ( $groupedNode instanceof $nodeClass )
|
||||
{
|
||||
$mergeNodes[ $groupedNode->_id ] = $groupedNode;
|
||||
}
|
||||
}
|
||||
unset( $this->value[ $index ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( count( $mergeNodes ) )
|
||||
{
|
||||
$this->value = array_merge( $this->value, $mergeNodes );
|
||||
}
|
||||
|
||||
foreach ( $this->value as $node )
|
||||
{
|
||||
$title = str_replace( "'", "'", htmlspecialchars( $node->_title, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', false ) );
|
||||
|
||||
$selected[ !( $node instanceof $nodeClass ) ? "{$node->_id}.s" : $node->_id ] = array( 'title' => $title, 'parents' => array_values( array_map( function( $val ){
|
||||
$suffix = "";
|
||||
if( !( $node instanceof $nodeClass ) )
|
||||
{
|
||||
$suffix = ( $node instanceof NodeGroup ) ? ".g" : ".s";
|
||||
}
|
||||
|
||||
$selected[ "{$node->_id}{$suffix}" ] = array( 'title' => $title, 'parents' => array_values( array_map( function( $val ){
|
||||
return str_replace( "'", "'", htmlspecialchars( $val->_title, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', false ) );
|
||||
}, iterator_to_array( $node->parents() ) ) ) );
|
||||
}
|
||||
}
|
||||
elseif ( $this->value instanceof Model )
|
||||
{
|
||||
$suffix = "";
|
||||
if( !( $this->value instanceof $nodeClass ) )
|
||||
{
|
||||
$suffix = ( $this->value instanceof NodeGroup ) ? ".g" : ".s";
|
||||
}
|
||||
|
||||
$class = get_class( $this->value );
|
||||
$title = str_replace( "'", "'", htmlspecialchars( $this->value->_title, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', false ) );
|
||||
$selected[ !( $this->value instanceof $nodeClass ) ? "{$this->value->_id}.s" : $this->value->_id ] = array( 'title' => $title, 'parents' => array_values( array_map( function( $val ){
|
||||
$selected[ "{$this->value->_id}{$suffix}" ] = array( 'title' => $title, 'parents' => array_values( array_map( function( $val ){
|
||||
return str_replace( "'", "'", htmlspecialchars( $val->_title, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', false ) );
|
||||
}, iterator_to_array( $this->value->parents() ) ) ) );
|
||||
}
|
||||
@@ -348,7 +402,7 @@ class Node extends FormAbstract
|
||||
}
|
||||
|
||||
/* Display */
|
||||
return Theme::i()->getTemplate( 'forms', 'core', 'global' )->node( $this->name, $selected, $this->options['multiple'], $this->options['url'], $nodeClass::$nodeTitle, $nodes, $this->options['zeroVal'], $selectable, $this->options['subnodes'], $this->options['togglePerm'], $this->options['toggleIds'], $disabledCallback, $this->options['zeroValTogglesOn'], $this->options['zeroValTogglesOff'], $this->options['autoPopulate'], $children, $this->options['class'], $this->options['where'], is_array( $this->options['disabledIds'] ) ? $this->options['disabledIds'] : array(), $this->options['noParentNodes'], $noParentNodes, $this->options['clubs'], $this->options['togglePermPBR'], $this->options['toggleIdsOff'], $loadMoreLink );
|
||||
return Theme::i()->getTemplate( 'forms', 'core', 'global' )->node( $this->name, $selected, $this->options['multiple'], $this->options['url'], $nodeClass::$nodeTitle, $nodes, $this->options['zeroVal'], $selectable, $this->options['subnodes'], $this->options['togglePerm'], $this->options['toggleIds'], $disabledCallback, $this->options['zeroValTogglesOn'], $this->options['zeroValTogglesOff'], $this->options['autoPopulate'], $children, $this->options['class'], $this->options['where'], is_array( $this->options['disabledIds'] ) ? $this->options['disabledIds'] : array(), $this->options['noParentNodes'], $noParentNodes, $this->options['clubs'], $this->options['togglePermPBR'], $this->options['toggleIdsOff'], $loadMoreLink, $nodeGroups );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -398,7 +452,7 @@ class Node extends FormAbstract
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if ( $depthLimit === NULL OR $depth < $depthLimit )
|
||||
{
|
||||
if ( !$totalLimit or ( ( $currentCount += $node->childrenCount( $showable, NULL, $this->options['subnodes'], $this->options['where'] ) ) < $totalLimit ) )
|
||||
@@ -463,25 +517,52 @@ class Node extends FormAbstract
|
||||
$showable = $this->_getShowable();
|
||||
|
||||
if ( $this->value and !( $this->value instanceof Model ) )
|
||||
{
|
||||
{
|
||||
$return = array();
|
||||
foreach ( is_array( $this->value ) ? $this->value : explode( ',', $this->value ) as $v )
|
||||
{
|
||||
if ( $v instanceof Model )
|
||||
{
|
||||
$return[ !( $v instanceof $nodeClass ) ? "s{$v->_id}" : $v->_id ] = $v;
|
||||
$prefix = '';
|
||||
if( !( $v instanceof $nodeClass ) )
|
||||
{
|
||||
$prefix = ( $v instanceof NodeGroup ) ? 'g' : 's';
|
||||
}
|
||||
$return [ $prefix . $v->_id ] = $v;
|
||||
}
|
||||
elseif( $v )
|
||||
{
|
||||
try
|
||||
{
|
||||
$exploded = explode( '.', $v );
|
||||
$classToUse = ( isset( $exploded[1] ) and $exploded[1] === 's' ) ? $nodeClass::$subnodeClass : $nodeClass;
|
||||
$node = $classToUse::load( $exploded[0] );
|
||||
|
||||
if ( ( !$showable or $node->can( $showable ) ) and !$selectable or ( is_string( $selectable ) and $node->can( $selectable ) ) or ( is_callable( $selectable ) and $selectable( $node ) ) )
|
||||
if( ( mb_substr( $v, 0, 1 ) === 'g' or mb_substr( $v, 0, 1 ) === 's' ) and is_numeric( mb_substr( $v, 1 ) ) )
|
||||
{
|
||||
$return[ isset( $exploded[1] ) ? "s{$node->_id}" : $node->_id ] = $node;
|
||||
$exploded = [
|
||||
mb_substr( $v, 1 ),
|
||||
mb_substr( $v, 0, 1 )
|
||||
];
|
||||
}
|
||||
else
|
||||
{
|
||||
$exploded = explode( '.', $v );
|
||||
}
|
||||
|
||||
if( isset( $exploded[1] ) )
|
||||
{
|
||||
$classToUse = ( $exploded[1] === 'g' ) ? NodeGroup::class : $nodeClass::$subnodeClass;
|
||||
}
|
||||
else
|
||||
{
|
||||
$classToUse = $nodeClass;
|
||||
}
|
||||
|
||||
$node = $classToUse::load( $exploded[0] );
|
||||
if( $classToUse == NodeGroup::class )
|
||||
{
|
||||
$return[ 'g' . $node->_id ] = $node;
|
||||
}
|
||||
elseif ( ( !$showable or $node->can( $showable ) ) and !$selectable or ( is_string( $selectable ) and $node->can( $selectable ) ) or ( is_callable( $selectable ) and $selectable( $node ) ) )
|
||||
{
|
||||
$return[ ( $exploded[1] ?? '' ) . $node->_id ] = $node;
|
||||
}
|
||||
}
|
||||
catch ( Exception $e ) {}
|
||||
|
||||
@@ -65,7 +65,8 @@ class Text extends TextArea
|
||||
'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.
|
||||
'alphabetical' => FALSE, // Force values to be sorted alphabetically.,
|
||||
'suggestionsOnly' => false, // Force values to come from the autocomplete suggestion form before submitting
|
||||
'suggestionsOnly' => false, // Force values to come from the autocomplete suggestion form before submitting,
|
||||
'separator' => null // Custom separator when multiple values are submitted in the request. Defaults to newline character.
|
||||
),
|
||||
'placeholder' => 'e.g. ...', // A placeholder (NB: Will only work on compatible browsers)
|
||||
'regex' => '/[A-Z]+/i', // RegEx of acceptable value
|
||||
@@ -203,7 +204,7 @@ class Text extends TextArea
|
||||
$return = trim( $return );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if ( isset( $this->options['autocomplete'] ) and $return !== null )
|
||||
{
|
||||
if ( is_array( $return ) )
|
||||
@@ -216,18 +217,19 @@ class Text extends TextArea
|
||||
{
|
||||
$return = htmlspecialchars_decode( $return, ENT_QUOTES );
|
||||
}
|
||||
|
||||
|
||||
if ( !is_array( $return ) and ( !isset( $this->options['autocomplete']['maxItems'] ) or $this->options['autocomplete']['maxItems'] != 1 ) )
|
||||
{
|
||||
$return = array_filter( array_map( 'trim', explode( "\n", $return ) ) );
|
||||
$separator = $this->options['autocomplete']['separator'] ?? "\n";
|
||||
$return = array_filter( array_map( 'trim', explode( $separator, $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 */
|
||||
if( $this->options['accountUsername'] )
|
||||
{
|
||||
@@ -268,17 +270,18 @@ class Text extends TextArea
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Format Value
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function formatValue(): mixed
|
||||
{
|
||||
{
|
||||
if ( $this->options['autocomplete'] !== NULL and ( !isset( $this->options['autocomplete']['maxItems'] ) or $this->options['autocomplete']['maxItems'] != 1 ) and !is_array( $this->value ) and $this->value !== NULL )
|
||||
{
|
||||
return array_filter( array_map( 'trim', explode( "\n", $this->value ) ) );
|
||||
$separator = $this->options['autocomplete']['separator'] ?? "\n";
|
||||
return array_filter( array_map( 'trim', explode( $separator, $this->value ) ) );
|
||||
}
|
||||
|
||||
return $this->value;
|
||||
|
||||
@@ -774,6 +774,13 @@ class Upload extends FormAbstract
|
||||
$hasBeenResized = $image->resizeToMax( $options['image']['maxWidth'] ?? NULL, $options['image']['maxHeight'] ?? NULL );
|
||||
}
|
||||
|
||||
/* If we rotated the image at this point, reset the image orientation so that
|
||||
we don't rotate it an extra time */
|
||||
if( $image->hasBeenRotated )
|
||||
{
|
||||
$image->setImageOrientation( 1 );
|
||||
}
|
||||
|
||||
/* If type JPG, image handler may strip meta data on image output */
|
||||
if( $hasBeenResized OR $image->hasBeenRotated OR $image->type == 'jpeg' )
|
||||
{
|
||||
|
||||
@@ -129,7 +129,7 @@ class Link extends MenuItem
|
||||
* @param string|NULL $id
|
||||
* @param string $identifier
|
||||
*/
|
||||
public function __construct( Url|string $url, string $languageString, string $css ='ipsMenu_item', array $dataAttributes = [], bool $opensDialog = FALSE, ?string $icon = NULL, string $id = NULL, string $identifier = '' )
|
||||
public function __construct( Url|string $url, string $languageString, string $css ='iDropdown__li', array $dataAttributes = [], bool $opensDialog = FALSE, ?string $icon = NULL, string $id = NULL, string $identifier = '' )
|
||||
{
|
||||
$this->url = $url;
|
||||
$this->title = $languageString;
|
||||
|
||||
@@ -34,7 +34,7 @@ class Menu
|
||||
/**
|
||||
* Menu Elements(items)
|
||||
*
|
||||
* @var array
|
||||
* @var MenuItem[]
|
||||
*/
|
||||
public array $elements = [];
|
||||
|
||||
@@ -86,7 +86,8 @@ class Menu
|
||||
public ?string $tooltip = NULL;
|
||||
|
||||
/**
|
||||
* @breif If this is true, menus with only one element will show as a button
|
||||
* @brief If this is true, menus with only one element will show as a button
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public bool $shrinkToButton = true;
|
||||
@@ -125,7 +126,6 @@ class Menu
|
||||
$elements = [];
|
||||
foreach( $this->elements as $key => $element )
|
||||
{
|
||||
/* @var MenuItem $element */
|
||||
$elements[ $key ] = $element;
|
||||
if( $element->menuItem == $after )
|
||||
{
|
||||
@@ -190,6 +190,7 @@ class Menu
|
||||
|
||||
/**
|
||||
* Adds HTML code to the stack
|
||||
*
|
||||
* @param string $html
|
||||
* @return $this
|
||||
*/
|
||||
@@ -202,7 +203,7 @@ class Menu
|
||||
/**
|
||||
* Returns all menu elements
|
||||
*
|
||||
* @return array
|
||||
* @return array<MenuItem>
|
||||
*/
|
||||
public function getLinks(): array
|
||||
{
|
||||
@@ -211,6 +212,7 @@ class Menu
|
||||
|
||||
/**
|
||||
* Returns a row wrapper which can contain custom HTML code
|
||||
*
|
||||
* @param string $content
|
||||
* @return string
|
||||
*/
|
||||
@@ -251,6 +253,7 @@ class Menu
|
||||
|
||||
/**
|
||||
* Returns the parsed menu
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string
|
||||
@@ -275,10 +278,10 @@ class Menu
|
||||
/**
|
||||
* Move an item to the top of the menu
|
||||
*
|
||||
* @param $item
|
||||
* @param MenuItem $item
|
||||
* @return void
|
||||
*/
|
||||
public function moveToStart( $item ): void
|
||||
public function moveToStart( MenuItem $item ): void
|
||||
{
|
||||
$currentElements = $this->elements;
|
||||
$newElements = [];
|
||||
|
||||
@@ -214,11 +214,10 @@ class Content extends Table
|
||||
$this->where[] = array($class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['container'] . '=?', $container->_id);
|
||||
}
|
||||
|
||||
$this->sortDirection = ( $this->sortDirection !== NULL ) ? $this->sortDirection : $container->_sortOrder;
|
||||
|
||||
if ( !$this->sortBy and ! empty( $container->_sortBy ) )
|
||||
{
|
||||
$this->sortBy = $class::$databaseTable . '.' . $class::$databasePrefix . $container->_sortBy;
|
||||
$this->sortDirection = $container->_sortOrder;
|
||||
}
|
||||
if ( !$this->filter )
|
||||
{
|
||||
@@ -264,7 +263,7 @@ class Content extends Table
|
||||
}
|
||||
|
||||
/* Set available sort options */
|
||||
foreach ( array( 'updated', 'last_comment', 'title', 'rating', 'date', 'num_comments', 'num_reviews', 'views' ) as $k )
|
||||
foreach ( array( 'updated', 'last_comment', 'title', 'rating', 'date', 'num_comments', 'num_reviews', 'views', 'num_helpful' ) as $k )
|
||||
{
|
||||
if ( isset( $class::$databaseColumnMap[ $k ] ) and !isset( $this->sortOptions[ $k ] ) )
|
||||
{
|
||||
@@ -395,6 +394,25 @@ class Content extends Table
|
||||
/* @var Item $class */
|
||||
/* @var array $databaseColumnMap */
|
||||
$sortBy = $this->sortBy . ' ' . ( mb_strtolower( $this->sortDirection ) == 'asc' ? 'asc' : 'desc' );
|
||||
|
||||
/* If we are sorting by rating, we want to make sure we take into account the number of ratings */
|
||||
if( array_key_exists( 'rating', $this->sortOptions ) and $this->sortBy == $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['rating'] )
|
||||
{
|
||||
if( isset( $class::$databaseColumnMap['rating_average'] ) )
|
||||
{
|
||||
$sortBy = $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['rating_average'] . ' ' . ( mb_strtolower( $this->sortDirection ) == 'asc' ? 'asc' : 'desc' );
|
||||
}
|
||||
|
||||
if( isset( $class::$databaseColumnMap['rating_hits'] ) )
|
||||
{
|
||||
$sortBy .= ',' . $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['rating_hits'] . ' ' . ( mb_strtolower( $this->sortDirection ) == 'asc' ? 'asc' : 'desc' );
|
||||
}
|
||||
elseif( isset( $class::$databaseColumnMap['num_reviews'] ) )
|
||||
{
|
||||
$sortBy .= ',' . $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['num_reviews'] . ' ' . ( mb_strtolower( $this->sortDirection ) == 'asc' ? 'asc' : 'desc' );
|
||||
}
|
||||
}
|
||||
|
||||
if ( IPS::classUsesTrait( $class, 'IPS\Content\Pinnable' ) and $this->honorPinned )
|
||||
{
|
||||
$column = $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['pinned'];
|
||||
|
||||
+34
-27
@@ -208,7 +208,7 @@ class Db extends Table
|
||||
{
|
||||
unset( $advancedSearchValues[ $k ] );
|
||||
}
|
||||
break;
|
||||
break;
|
||||
case SEARCH_DATE_RANGE:
|
||||
$timezone = ( Member::loggedIn()->timezone ? new DateTimeZone( Member::loggedIn()->timezone ) : NULL );
|
||||
|
||||
@@ -261,37 +261,44 @@ class Db extends Table
|
||||
break;
|
||||
|
||||
case SEARCH_NODE:
|
||||
$nodeClass = $options['class'];
|
||||
$prop = $options['searchProp'] ?? '_id';
|
||||
if ( !is_array( $v ) )
|
||||
|
||||
if( $v )
|
||||
{
|
||||
$v = array( $v );
|
||||
}
|
||||
|
||||
$values = array();
|
||||
foreach ( $v as $_v )
|
||||
{
|
||||
if ( !is_object( $_v ) )
|
||||
$nodeClass = $options[ 'class' ];
|
||||
$prop = $options[ 'searchProp' ]??'_id';
|
||||
if( !is_array( $v ) )
|
||||
{
|
||||
if ( mb_substr( $_v, 0, 2 ) === 's.' )
|
||||
{
|
||||
$nodeClass = $nodeClass::$subnodeClass;
|
||||
$_v = mb_substr( $_v, 2 );
|
||||
}
|
||||
try
|
||||
{
|
||||
$_v = $nodeClass::load( $_v );
|
||||
}
|
||||
catch ( OutOfRangeException $e )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
$v = [$v];
|
||||
}
|
||||
$values[] = $_v->$prop;
|
||||
$values = [];
|
||||
foreach( $v as $_v )
|
||||
{
|
||||
if( !is_object( $_v ) )
|
||||
{
|
||||
if( mb_substr( $_v, 0, 2 ) === 's.' )
|
||||
{
|
||||
$nodeClass = $nodeClass::$subnodeClass;
|
||||
$_v = mb_substr( $_v, 2 );
|
||||
}
|
||||
try
|
||||
{
|
||||
$_v = $nodeClass::load( $_v );
|
||||
}
|
||||
catch( OutOfRangeException $e )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$values[] = $_v->$prop;
|
||||
}
|
||||
$where[] = [$this->db->in( $k, $values )];
|
||||
}
|
||||
$where[] = array( $this->db->in( $k, $values ) );
|
||||
else
|
||||
{
|
||||
unset( $advancedSearchValues[ $k ] );
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case SEARCH_NUMERIC:
|
||||
case SEARCH_NUMERIC_TEXT:
|
||||
switch ( $v[0] )
|
||||
|
||||
Reference in new issue
Block a user