Version 5.0.0 beta 11.1
This commit is contained in:
1 parent
264eeb97e0
commit
90e8c7e903
466 files changed
+7796
-2394
No files matched your search
+27
-24
@@ -3618,36 +3618,39 @@ abstract class Item extends Content
|
||||
|
||||
$count = $this->mapped('num_reviews');
|
||||
|
||||
if( $this->canViewHiddenReviews() )
|
||||
if( IPS::classUsesTrait( $this, Hideable::class ) )
|
||||
{
|
||||
if ( isset( static::$databaseColumnMap['hidden_reviews'] ) )
|
||||
if( $this->canViewHiddenReviews() )
|
||||
{
|
||||
$count += $this->mapped('hidden_reviews');
|
||||
if ( isset( static::$databaseColumnMap['hidden_reviews'] ) )
|
||||
{
|
||||
$count += $this->mapped('hidden_reviews');
|
||||
}
|
||||
if ( isset( static::$databaseColumnMap['unapproved_reviews'] ) )
|
||||
{
|
||||
$count += $this->mapped('unapproved_reviews');
|
||||
}
|
||||
}
|
||||
if ( isset( static::$databaseColumnMap['unapproved_reviews'] ) )
|
||||
elseif ( isset( static::$databaseColumnMap['unapproved_reviews'] ) and Member::loggedIn()->member_id and $this->mapped('unapproved_reviews') )
|
||||
{
|
||||
$count += $this->mapped('unapproved_reviews');
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$class = static::$reviewClass;
|
||||
/* @var $databaseColumnMap array */
|
||||
$authorCol = $class::$databasePrefix . $class::$databaseColumnMap['author'];
|
||||
$where = array( array( $class::$databasePrefix . $class::$databaseColumnMap['item'] . '=?', $this->$idColumn ) );
|
||||
if ( isset( $class::$databaseColumnMap['approved'] ) )
|
||||
{
|
||||
$col = $class::$databasePrefix . $class::$databaseColumnMap['approved'];
|
||||
$where[] = array( "{$col}=0 AND {$authorCol}=" . Member::loggedIn()->member_id );
|
||||
}
|
||||
elseif( isset( $class::$databaseColumnMap['hidden'] ) )
|
||||
{
|
||||
$col = $class::$databasePrefix . $class::$databaseColumnMap['hidden'];
|
||||
$where[] = array( "{$col}=1 AND {$authorCol}=" . Member::loggedIn()->member_id );
|
||||
}
|
||||
$count += Db::i()->select( 'COUNT(*)', $class::$databaseTable, $where )->first();
|
||||
}
|
||||
}
|
||||
elseif ( isset( static::$databaseColumnMap['unapproved_reviews'] ) and Member::loggedIn()->member_id and $this->mapped('unapproved_reviews') )
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$class = static::$reviewClass;
|
||||
/* @var $databaseColumnMap array */
|
||||
$authorCol = $class::$databasePrefix . $class::$databaseColumnMap['author'];
|
||||
$where = array( array( $class::$databasePrefix . $class::$databaseColumnMap['item'] . '=?', $this->$idColumn ) );
|
||||
if ( isset( $class::$databaseColumnMap['approved'] ) )
|
||||
{
|
||||
$col = $class::$databasePrefix . $class::$databaseColumnMap['approved'];
|
||||
$where[] = array( "{$col}=0 AND {$authorCol}=" . Member::loggedIn()->member_id );
|
||||
}
|
||||
elseif( isset( $class::$databaseColumnMap['hidden'] ) )
|
||||
{
|
||||
$col = $class::$databasePrefix . $class::$databaseColumnMap['hidden'];
|
||||
$where[] = array( "{$col}=1 AND {$authorCol}=" . Member::loggedIn()->member_id );
|
||||
}
|
||||
$count += Db::i()->select( 'COUNT(*)', $class::$databaseTable, $where )->first();
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
@@ -308,8 +308,8 @@ class Amazon extends File
|
||||
{
|
||||
parent::load();
|
||||
|
||||
/* Change the public URL to the gzipped version if the browser supports it */
|
||||
if ( $this->needsGzipVersion() and ( isset( $_SERVER['HTTP_ACCEPT_ENCODING'] ) and strpos( $_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip' ) !== false ) )
|
||||
/* Change the public URL to the gzipped version if we force gzip versions of this file. */
|
||||
if ( $this->needsGzipVersion() )
|
||||
{
|
||||
$this->url = new Url( $this->url . '.gz' );
|
||||
}
|
||||
|
||||
@@ -0,0 +1,325 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief File Handler: Backblaze
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 10 Oct 2024
|
||||
*/
|
||||
|
||||
namespace IPS\File;
|
||||
|
||||
/* Set use statements to make Uncle Matt happy */
|
||||
use IPS\cloud\Platform\Bridge;
|
||||
use IPS\DateTime AS DT;
|
||||
use IPS\Data\Store;
|
||||
use IPS\Db;
|
||||
use IPS\File;
|
||||
use IPS\File\Amazon;
|
||||
use IPS\Http\Url;
|
||||
use IPS\Http\Response;
|
||||
use IPS\Request;
|
||||
use IPS\Http\Request\Exception as RequestException;
|
||||
use IPS\Member;
|
||||
use DateInterval;
|
||||
|
||||
use function array_filter;
|
||||
use function array_map;
|
||||
use function debug_backtrace;
|
||||
use function defined;
|
||||
use function header;
|
||||
use function in_array;
|
||||
use function mb_strstr;
|
||||
use function trim;
|
||||
use function strlen;
|
||||
|
||||
use const ARRAY_FILTER_USE_KEY;
|
||||
use const DEBUG_BACKTRACE_IGNORE_ARGS;
|
||||
|
||||
use const IPS\VERY_LONG_REQUEST_TIMEOUT;
|
||||
|
||||
/* 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 Backblaze extends Amazon
|
||||
{
|
||||
/**
|
||||
* Settings
|
||||
*
|
||||
* @param array $configuration Configuration if editing a setting, or array() if creating a setting.
|
||||
* @return array
|
||||
*/
|
||||
public static function settings( array $configuration=array() ): array
|
||||
{
|
||||
$default = ( isset( $configuration['custom_url'] ) and ! empty( $configuration['custom_url'] ) ) ? TRUE : FALSE;
|
||||
$return = parent::settings( $configuration );
|
||||
unset( $return['toggle'], $return['custom_url'] );
|
||||
|
||||
$return['endpoint'] = array( 'type' => 'Text', 'default' => 's3.backblazeb2.com' );
|
||||
$return['toggle'] = array( 'type' => 'YesNo', 'default' => $default, 'options' => array(
|
||||
'togglesOn' => array( 'Backblaze_custom_url' )
|
||||
) );
|
||||
$return['custom_url'] = array( 'type' => 'Text', 'default' => '' );
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display name
|
||||
*
|
||||
* @param array $settings Configuration settings
|
||||
* @return string
|
||||
*/
|
||||
public static function displayName( array $settings ): string
|
||||
{
|
||||
return Member::loggedIn()->language()->addToStack( 'filehandler_display_name', FALSE, array( 'sprintf' => array( Member::loggedIn()->language()->addToStack('filehandler__Backblaze'), $settings['bucket'] ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign and make request
|
||||
*
|
||||
* @param string $uri The URI (relative to the bucket)
|
||||
* @param string $verb The HTTP verb to use
|
||||
* @param array $configuration The configuration for this instance
|
||||
* @param int $configurationId The configuration ID
|
||||
* @param string|null $content The content to send
|
||||
* @param string|null $storageExtension Storage extension
|
||||
* @param bool $skipExtraChecks Skips the endpoint check (to prevent infinite looping)
|
||||
* @param bool $isPrivate This can be set to true to access/store private files (i.e. that are not publicly readable)
|
||||
* @param array $queryString Array of key => value pairs
|
||||
* @return \IPS\Http\Response
|
||||
* @throws \IPS\Http\Request\Exception
|
||||
*/
|
||||
protected static function makeRequest( string $uri, string $verb, array $configuration, ?int $configurationId, ?string $content=NULL, ?string $storageExtension=NULL, bool $skipExtraChecks=FALSE, ?bool $isPrivate=false, array $queryString=array() ): Response
|
||||
{
|
||||
/* Amazon requires filename characters to be properly encoded - let's urlencode the filename here */
|
||||
$uriPieces = explode( '/', $uri );
|
||||
$filename = array_pop( $uriPieces );
|
||||
$uri = ltrim( implode( '/', $uriPieces ) . '/' . rawurlencode( $filename ), '/' );
|
||||
|
||||
/* Build a request */
|
||||
$url = Url::external( static::buildBaseUrl( $configuration ) . $uri );
|
||||
if ( $queryString )
|
||||
{
|
||||
$url = $url->setQueryString( $queryString );
|
||||
}
|
||||
$request = $url->request( VERY_LONG_REQUEST_TIMEOUT, NULL, FALSE ); # Amazon will send a 301 header code, but no Location header, if we need to try another endpoint
|
||||
|
||||
/* When using virtual hosted–style buckets with SSL, the SSL wild card certificate only matches buckets that do not contain periods. To work around this, use HTTP or write your own certificate verification logic. @link http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html */
|
||||
if ( Request::i()->isSecure() and mb_strstr( $configuration['bucket'], '.' ) )
|
||||
{
|
||||
$request->sslCheck( FALSE );
|
||||
}
|
||||
|
||||
/* Set headers */
|
||||
$headers = array(
|
||||
'Content-Type' => File::getMimeType( $uri ),
|
||||
'Content-MD5' => base64_encode( md5( $content, TRUE ) ),
|
||||
);
|
||||
|
||||
/* If uploading a file, need to specify length and cache control */
|
||||
if( mb_strtoupper( $verb ) === 'PUT' )
|
||||
{
|
||||
$headers['Content-Length'] = strlen( $content );
|
||||
|
||||
$cacheSeconds = 3600 * 24 * 365;
|
||||
|
||||
/* Custom Cache-Control */
|
||||
if( $storageExtension !== NULL AND mb_strpos( $storageExtension, '_' ) !== FALSE )
|
||||
{
|
||||
$bits = explode( '_', $storageExtension );
|
||||
$class = '\IPS\\' . $bits[0] . '\extensions\core\FileStorage\\' . $bits[1];
|
||||
|
||||
if ( isset( $class::$cacheControlTtl ) and $class::$cacheControlTtl )
|
||||
{
|
||||
$cacheSeconds = $class::$cacheControlTtl;
|
||||
}
|
||||
}
|
||||
|
||||
$headers['Cache-Control'] = 'public, max-age=' . $cacheSeconds;
|
||||
}
|
||||
|
||||
/* We need to strip query string parameters for the signature, but not always (e.g. a subresource such as ?acl needs to be included and multi-
|
||||
object delete requests must include the query string params). Let the callee decide to do this or not. */
|
||||
if( isset( $configuration['_strip_querystring'] ) AND $configuration['_strip_querystring'] === TRUE )
|
||||
{
|
||||
$uri = preg_replace( "/^(.*?)\?.*$/", "$1", $uri );
|
||||
}
|
||||
|
||||
/* Sign the request */
|
||||
$authorization = static::signature( $configuration, $verb, $uri, $headers, $queryString, $content );
|
||||
$headers['Authorization'] = $authorization;
|
||||
unset( $headers['Host'] );
|
||||
$request->setHeaders( $headers );
|
||||
|
||||
/* Make the request */
|
||||
$verb = mb_strtolower( $verb );
|
||||
$response = $request->$verb( $content );
|
||||
|
||||
/* If we are skipping extra checks, return response now */
|
||||
if( $skipExtraChecks )
|
||||
{
|
||||
return $response;
|
||||
}
|
||||
|
||||
/* Change endpoint if necessary */
|
||||
if ( $response->httpResponseCode == 301 )
|
||||
{
|
||||
$xml = $response->decodeXml();
|
||||
if ( isset( $xml->Endpoint ) )
|
||||
{
|
||||
/* We have an endpoint, but if we called s3.amazonaws.com then it might be wrong. Try to detect the correct one. */
|
||||
$configuration['endpoint'] = 's3-us-west-1.amazonaws.com';
|
||||
|
||||
$endpointResponse = static::makeRequest( $uri, $verb, $configuration, $configurationId, $content, NULL, TRUE );
|
||||
$update = FALSE;
|
||||
|
||||
/* If the response code is 200, we got lucky and that's our endpoint */
|
||||
if( $endpointResponse->httpResponseCode == 200 )
|
||||
{
|
||||
$update = TRUE;
|
||||
}
|
||||
/* If it's a 301 response, we should be able to pull out the correct endpoint now */
|
||||
elseif( $endpointResponse->httpResponseCode == 301 )
|
||||
{
|
||||
$xml = $endpointResponse->decodeXml();
|
||||
if ( isset( $xml->Endpoint ) )
|
||||
{
|
||||
/* Strip out the bucket from the endpoint */
|
||||
$configuration['endpoint'] = preg_replace( '/^' . preg_quote( $configuration['bucket'], '/' ) . '\./', '', (string) $xml->Endpoint );
|
||||
$update = TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
/* If we need to update, do it now and return the result */
|
||||
if( $update === TRUE )
|
||||
{
|
||||
static::$updatedEndpoint = $configuration['endpoint'];
|
||||
|
||||
if ( $configurationId )
|
||||
{
|
||||
Db::i()->update( 'core_file_storage', array( 'configuration' => json_encode( $configuration ) ), array( "id=?", $configurationId ) );
|
||||
unset( Store::i()->storageConfigurations );
|
||||
}
|
||||
}
|
||||
|
||||
return static::makeRequest( $uri, $verb, $configuration, $configurationId, $content );
|
||||
}
|
||||
}
|
||||
|
||||
/* Change region if necessary */
|
||||
if ( $response->httpResponseCode == 400 )
|
||||
{
|
||||
try
|
||||
{
|
||||
$xml = $response->decodeXml();
|
||||
if ( isset( $xml->Region ) )
|
||||
{
|
||||
$configuration['region'] = (string) $xml->Region;
|
||||
if ( $configurationId )
|
||||
{
|
||||
Db::i()->update( 'core_file_storage', array( 'configuration' => json_encode( $configuration ) ), array( 'id=?', $configurationId ) );
|
||||
unset( Store::i()->storageConfigurations );
|
||||
}
|
||||
return static::makeRequest( $uri, $verb, $configuration, $configurationId, $content );
|
||||
}
|
||||
}
|
||||
catch ( \Exception $e ) { }
|
||||
}
|
||||
|
||||
/* Return */
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a temporary download URL the user can be redirected to
|
||||
*
|
||||
* @param $validForSeconds int The number of seconds the link should be valid for
|
||||
* @return Url
|
||||
*/
|
||||
public function generateTemporaryDownloadUrl( int $validForSeconds = 1200 ): Url
|
||||
{
|
||||
if ( $url = Bridge::i()->b2GenerateTemporaryDownloadUrl( $this, $validForSeconds ) and str_starts_with( $this->configuration['bucket'], 'cloud3-media' ) )
|
||||
{
|
||||
return $url;
|
||||
}
|
||||
|
||||
$expires = ( new DT )->add( new DateInterval( "PT{$validForSeconds}S" ) );
|
||||
|
||||
/* Authorize */
|
||||
$authorize = Url::external( "https://api.backblazeb2.com/b2api/v3/b2_authorize_account" )
|
||||
->request()
|
||||
->login( $this->configuration['access_key'], $this->configuration['secret_key'] )
|
||||
->get();
|
||||
$response = $authorize->decodeJson();
|
||||
if ( $authorize->httpResponseCode !== '200' )
|
||||
{
|
||||
throw new Exception( $response['code'] );
|
||||
}
|
||||
|
||||
$download = URL::external( "{$response['apiInfo']['storageApi']['apiUrl']}/b2api/v3/get_download_authorization" )
|
||||
->request()
|
||||
->setHeaders( array(
|
||||
'Authorization' => $response['authorizationToken']
|
||||
) )
|
||||
->post( json_encode( array(
|
||||
'bucketId' => $response['apiInfo']['storageApi']['bucketId'],
|
||||
'fileNamePrefix' => "{$this->container}/",
|
||||
'validDurationInSeconds' => $expires->getTimestamp(),
|
||||
'b2ContentDisposition' => 'attachment; filename="' . $this->originalFilename . '"',
|
||||
'b2Expires' => $expires->rfc1123(),
|
||||
'b2ContentType' => static::getMimeType( $this->originalFilename ),
|
||||
|
||||
) ) )
|
||||
->decodeJson();
|
||||
|
||||
$query = http_build_query( array(
|
||||
'Authorization' => $download['authorizationToken'],
|
||||
'b2ContentDisposition' => 'attachment; filename="' . $this->originalFilename . '"',
|
||||
'b2Expires' => $expires->rfc1123(),
|
||||
'b2ContentType' => static::getMimeType( $this->originalFilename ),
|
||||
) );
|
||||
|
||||
return Url::external( "{$response['apiInfo']['storageApi']['downloadUrl']}/file/{$this->configuration['bucket']}/{$this->container}/{$this->filename}?{$query}" );
|
||||
}
|
||||
|
||||
/**
|
||||
* Build up the base Amazon URL
|
||||
* @param array $configuration Configuration data
|
||||
* @return string
|
||||
*/
|
||||
public static function buildBaseUrl( array $configuration ): string
|
||||
{
|
||||
if ( mb_strstr( $configuration['bucket'], '.' ) )
|
||||
{
|
||||
return (
|
||||
Request::i()->isSecure() ? "https" : "http" ) . "://"
|
||||
. ( isset( $configuration['endpoint'] ) ? $configuration['endpoint'] : "s3.backblazeb2.com" )
|
||||
. "/{$configuration['bucket']}"
|
||||
. static::bucketPath( $configuration )
|
||||
. '/';
|
||||
}
|
||||
else
|
||||
{
|
||||
return (
|
||||
Request::i()->isSecure() ? "https" : "http" ) . "://{$configuration['bucket']}."
|
||||
. ( isset( $configuration['endpoint'] ) ? $configuration['endpoint'] : "s3.backblazeb2.com" )
|
||||
. static::bucketPath( $configuration )
|
||||
. '/';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not allow gzip versions for Backblaze
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function needsGzipVersion(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -78,6 +78,7 @@ abstract class File
|
||||
}
|
||||
|
||||
$return['Amazon'] = 'IPS\\File\\Amazon';
|
||||
$return['Backblaze'] = 'IPS\\File\\Backblaze';
|
||||
|
||||
if ( $current and $current['method'] === 'Ftp' )
|
||||
{
|
||||
|
||||
@@ -147,7 +147,7 @@ class Color extends FormAbstract
|
||||
{
|
||||
if ( @$module['isEntry'] and isset( $module['file'] ) )
|
||||
{
|
||||
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'iro/' . $module['file'], 'core', 'interface' ) );
|
||||
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'static/iro/' . $module['file'], 'core', 'interface' ) );
|
||||
}
|
||||
}
|
||||
$loaded = true;
|
||||
|
||||
@@ -270,7 +270,7 @@ class Editor extends FormAbstract
|
||||
if ( !@$entry['isEntry'] ) {
|
||||
continue;
|
||||
}
|
||||
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, array( Url::internal( "applications/core/interface/tiptap/{$entry['file']}", 'none', NULL, array(), Url::PROTOCOL_RELATIVE ) ) );
|
||||
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, array( Url::internal( "applications/core/interface/static/tiptap/{$entry['file']}", 'none', NULL, array(), Url::PROTOCOL_RELATIVE ) ) );
|
||||
}
|
||||
|
||||
Output::i()->jsVars['editorPreloading'] = true;
|
||||
|
||||
@@ -14,7 +14,6 @@ class Link extends MenuItem
|
||||
* @var Url|string
|
||||
*/
|
||||
public Url|string $url = '';
|
||||
public string $extraHtml = '';
|
||||
public string $identifier = '';
|
||||
public string $notificationIcon = '';
|
||||
public array $dataAttributes = [];
|
||||
@@ -64,6 +63,40 @@ class Link extends MenuItem
|
||||
*/
|
||||
public function opensDialog( string $title = NULL, string $size = 'medium', bool $destruct = FALSE, string $contentSelector = NULL, bool $remoteSubmit = FALSE ): self
|
||||
{
|
||||
/* Check data attributes to see if they already exist, they may have been passed in the constructor */
|
||||
foreach( $this->dataAttributes as $key => $value )
|
||||
{
|
||||
switch( $key )
|
||||
{
|
||||
case 'data-ipsDialog-title':
|
||||
if( $title === null )
|
||||
{
|
||||
$title = $value;
|
||||
unset( $this->dataAttributes['data-ipsDialog-title'] );
|
||||
}
|
||||
break;
|
||||
case 'data-ipsDialog-size':
|
||||
$size = $value;
|
||||
unset( $this->dataAttributes['data-ipsDialog-size'] );
|
||||
break;
|
||||
case 'data-ipsDialog-content':
|
||||
if( $contentSelector === null )
|
||||
{
|
||||
$contentSelector = $value;
|
||||
unset( $this->dataAttributes['data-ipsDialog-content'] );
|
||||
}
|
||||
break;
|
||||
case 'data-ipsDialog-destructOnClose':
|
||||
$destruct = $value;
|
||||
unset( $this->dataAttributes['data-ipsDialog-destructOnClose'] );
|
||||
break;
|
||||
case 'data-ipsDialog-remoteSubmit':
|
||||
$remoteSubmit = $value;
|
||||
unset( $this->dataAttributes['data-ipsDialog-remoteSubmit'] );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->addAttribute( 'data-ipsDialog')
|
||||
->addAttribute( 'data-ipsDialog-size', $size )
|
||||
->addAttribute( 'data-ipsDialog-title', Member::loggedIn()->language()->addToStack( $title ?? $this->title ) );
|
||||
@@ -103,12 +136,12 @@ class Link extends MenuItem
|
||||
$this->css = $css;
|
||||
$this->dataAttributes = $dataAttributes;
|
||||
$this->icon = $icon;
|
||||
|
||||
$this->menuItem = $identifier ?: $languageString;
|
||||
$this->identifier = $identifier;
|
||||
$this->menuItem = $this->identifier ?: $this->title;
|
||||
|
||||
if( !$id )
|
||||
{
|
||||
$id = 'menuLink_' . md5($languageString ) . '_' . $this->identifier;
|
||||
$id = 'menuLink_' . md5( $this->title ) . '_' . $this->identifier;
|
||||
}
|
||||
$this->id = $id;
|
||||
if( $opensDialog )
|
||||
@@ -116,7 +149,7 @@ class Link extends MenuItem
|
||||
$this->opensDialog();
|
||||
}
|
||||
|
||||
$this->addAttribute( 'data-menuItem', $languageString);
|
||||
$this->addAttribute( 'data-menuItem', $this->menuItem );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2132,7 +2132,7 @@ class Club extends ActiveRecord implements Embeddable
|
||||
}
|
||||
|
||||
/* Members */
|
||||
if ( $this->show_membertab == 'member' AND in_array( $this->memberStatus( $member ), array( Club::STATUS_MEMBER, Club::STATUS_INVITED, Club::STATUS_INVITED_BYPASSING_PAYMENT, Club::STATUS_EXPIRED, Club::STATUS_EXPIRED_MODERATOR ) ) )
|
||||
if ( $this->show_membertab == 'member' AND in_array( $this->memberStatus( $member ), array( Club::STATUS_MEMBER, Club::STATUS_INVITED, Club::STATUS_INVITED_BYPASSING_PAYMENT, Club::STATUS_EXPIRED, Club::STATUS_EXPIRED_MODERATOR, Club::STATUS_MODERATOR ) ) )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -382,10 +382,16 @@ class Output
|
||||
public function js( string $file, string $app=NULL, string $location=NULL ): array
|
||||
{
|
||||
$file = trim( $file, '/' );
|
||||
|
||||
/* Legacy code support. The following directories were moved to /static */
|
||||
if ( ( $app === "core" or $app === null) and $location === "interface" and preg_match( "/^(?:codemirror|fontawesome)/", $file ) )
|
||||
{
|
||||
$file = "static/" . $file;
|
||||
}
|
||||
|
||||
if ( $location === 'interface' AND mb_substr( $file, -3 ) === '.js' )
|
||||
{
|
||||
return array( rtrim( Url::baseUrl( Url::PROTOCOL_RELATIVE ), '/' ) . "/applications/{$app}/interface/{$file}?v=" . ( defined( '\IPS\CACHEBUST_KEY' ) ? CACHEBUST_KEY : time() ) );
|
||||
return array( rtrim( Url::baseUrl( Url::PROTOCOL_RELATIVE ), '/' ) . "/applications/{$app}/interface/{$file}?v=" . Javascript::javascriptCacheBustKey() );
|
||||
}
|
||||
elseif (IN_DEV)
|
||||
{
|
||||
@@ -1401,7 +1407,7 @@ class Output
|
||||
{
|
||||
if( Request::i()->isAjax() )
|
||||
{
|
||||
if ( $message !== '' )
|
||||
if( !empty( $message ) )
|
||||
{
|
||||
$message = Member::loggedIn()->language()->checkKeyExists( $message ) ? Member::loggedIn()->language()->addToStack( $message ) : $message;
|
||||
Member::loggedIn()->language()->parseOutputForDisplay( $message );
|
||||
|
||||
@@ -260,10 +260,10 @@ class Parser
|
||||
/* Get acronyms */
|
||||
if ( $parseAcronyms )
|
||||
{
|
||||
$this->caseSensitiveAcronyms = iterator_to_array( Db::i()->select( array( 'a_short', 'a_long', 'a_type' ), 'core_acronyms', array( 'a_casesensitive=1' ) )->setKeyField( 'a_short' ) );
|
||||
$this->caseSensitiveAcronyms = iterator_to_array( Db::i()->select( array( 'a_short', 'a_long', 'a_type' ), 'core_acronyms', array( 'a_casesensitive=1' ), 'LENGTH(a_short) DESC' )->setKeyField( 'a_short' ) );
|
||||
|
||||
$this->caseInsensitiveAcronyms = array();
|
||||
foreach ( Db::i()->select( array( 'a_short', 'a_long', 'a_type' ), 'core_acronyms', array( 'a_casesensitive=0' ) )->setKeyField( 'a_short' ) as $k => $v )
|
||||
foreach ( Db::i()->select( array( 'a_short', 'a_long', 'a_type' ), 'core_acronyms', array( 'a_casesensitive=0' ), 'LENGTH(a_short) DESC' )->setKeyField( 'a_short' ) as $k => $v )
|
||||
{
|
||||
$this->caseInsensitiveAcronyms[ mb_strtolower( $k ) ] = $v;
|
||||
}
|
||||
|
||||
@@ -661,7 +661,7 @@ EOF;
|
||||
*/
|
||||
public function form( Form &$form ): void
|
||||
{
|
||||
//$form->class = 'ipsForm--vertical ipsForm--custom-template ipsForm_fullWidth';
|
||||
//$form->class = 'ipsForm--vertical ipsForm--custom-template ipsForm--fullWidth';
|
||||
$form->attributes['data-controller'] = 'core.admin.customization.customTemplatesForm';
|
||||
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'admin_customization.js', 'core', 'admin' ) );
|
||||
|
||||
|
||||
@@ -35,7 +35,9 @@ use function intval;
|
||||
use function is_array;
|
||||
use function is_dir;
|
||||
use function is_string;
|
||||
use function preg_match;
|
||||
use function str_replace;
|
||||
use function str_starts_with;
|
||||
use function stristr;
|
||||
use function strstr;
|
||||
use const IPS\DEFAULT_THEME_ID;
|
||||
@@ -221,6 +223,12 @@ class Theme extends SystemTheme
|
||||
|
||||
if ( $location === 'interface' )
|
||||
{
|
||||
/* Legacy code support. The following directories were moved to /static */
|
||||
if ( ( $app === "core" or $app === null) and preg_match( "/^(?:codemirror|fontawesome)/", $file ) )
|
||||
{
|
||||
$file = "static/" . $file;
|
||||
}
|
||||
|
||||
$path = ROOT_PATH . "/applications/{$app}/interface/{$file}";
|
||||
}
|
||||
else
|
||||
@@ -228,6 +236,8 @@ class Theme extends SystemTheme
|
||||
$path = static::_getCssPath($app, $location, $file);
|
||||
}
|
||||
|
||||
$isStaticInterface = (bool) ( $location === "interface" and ( $app === "core" or $app === null) and str_starts_with( $file, "static/" ) );
|
||||
|
||||
if ( isset( self::$buildGrouping['css'][ $app ][ $location ] ) AND is_array( self::$buildGrouping['css'][ $app ][ $location ] ) )
|
||||
{
|
||||
foreach( self::$buildGrouping['css'][ $app ][ $location ] as $buildPath )
|
||||
@@ -251,7 +261,7 @@ class Theme extends SystemTheme
|
||||
/* Load css/location/folderName/folderName.css first */
|
||||
if ( is_file( $path . '/' . $fileName ) )
|
||||
{
|
||||
$return[] = str_replace( array( 'http://', 'https://' ), '//', Settings::i()->base_url ) . "applications/core/interface/css/css.php?css=" . ( str_replace( ROOT_PATH . '/', '', static::_getCssPath( $app, $location, $file ) ) ) . $fileName;
|
||||
$return[] = str_replace( array( 'http://', 'https://' ), '//', Settings::i()->base_url ) . ( $isStaticInterface ? "" : "applications/core/interface/css/css.php?css=" ) . ( str_replace( ROOT_PATH . '/', '', static::_getCssPath( $app, $location, $file ) ) ) . $fileName;
|
||||
}
|
||||
|
||||
$csses = array();
|
||||
@@ -267,11 +277,11 @@ class Theme extends SystemTheme
|
||||
|
||||
if ( count( $csses ) )
|
||||
{
|
||||
if( DEV_DEBUG_CSS )
|
||||
if( DEV_DEBUG_CSS or $isStaticInterface )
|
||||
{
|
||||
foreach( $csses as $cssFile )
|
||||
{
|
||||
$return[] = str_replace( array( 'http://', 'https://' ), '//', Settings::i()->base_url ) . "applications/core/interface/css/css.php?css=" . $cssFile;
|
||||
$return[] = str_replace( array( 'http://', 'https://' ), '//', Settings::i()->base_url ) . ( $isStaticInterface ? "" : "applications/core/interface/css/css.php?css=" ) . $cssFile;
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -282,7 +292,7 @@ class Theme extends SystemTheme
|
||||
}
|
||||
elseif ( file_exists( $path ) )
|
||||
{
|
||||
$return[] = str_replace( ROOT_PATH . '/', '', str_replace( array( 'http://', 'https://' ), '//', Settings::i()->base_url ) . "applications/core/interface/css/css.php?css=" . $path );
|
||||
$return[] = str_replace( ROOT_PATH . '/', '', str_replace( array( 'http://', 'https://' ), '//', Settings::i()->base_url ) . ( $isStaticInterface ? "" : "applications/core/interface/css/css.php?css=" ) . $path );
|
||||
}
|
||||
|
||||
return $return;
|
||||
|
||||
@@ -611,6 +611,12 @@ class Theme extends Model
|
||||
|
||||
if ( $location === 'interface' )
|
||||
{
|
||||
/* Legacy code support. The following directories were moved to /static */
|
||||
if ( ( $app === "core" or $app === null) and preg_match( "/^(?:codemirror|fontawesome)/", $file ) )
|
||||
{
|
||||
$file = "static/" . $file;
|
||||
}
|
||||
|
||||
static::$usedCss[ $cacheKey ] = array( rtrim( Url::baseUrl( Url::PROTOCOL_RELATIVE ), '/' ) . "/applications/{$app}/interface/{$file}" );
|
||||
return static::$usedCss[ $cacheKey ];
|
||||
}
|
||||
|
||||
+106
-39
@@ -16,6 +16,7 @@ namespace IPS\Widget;
|
||||
|
||||
use Exception;
|
||||
use IPS\Application;
|
||||
use IPS\cms\widgets\Database as DatabaseWidget;
|
||||
use IPS\Db;
|
||||
use IPS\Lang;
|
||||
use IPS\Member;
|
||||
@@ -196,19 +197,33 @@ class Area
|
||||
$this->parentData = $parent->toArray( false );
|
||||
$this->parentData['styles'] = $parent->styles();
|
||||
$this->depth = $parent->depth + 1;
|
||||
}
|
||||
|
||||
if( isset( $data['widgets'] ) and count( $data['widgets'] ) )
|
||||
{
|
||||
/* If this has widgets, set the orientation for this level */
|
||||
if( isset( $data['orientation'] ) )
|
||||
/* Widgets don't go on the root level, they go one level down */
|
||||
if( isset( $data['widgets'] ) and count( $data['widgets'] ) )
|
||||
{
|
||||
$this->setOption( 'orientation', $data['orientation'] );
|
||||
}
|
||||
/* If this has widgets, set the orientation for this level */
|
||||
if( isset( $data['orientation'] ) )
|
||||
{
|
||||
$this->setOption( 'orientation', $data['orientation'] );
|
||||
}
|
||||
|
||||
foreach( $data['widgets'] as $key => $widget )
|
||||
{
|
||||
$this->addWidget( $widget );
|
||||
}
|
||||
}
|
||||
}
|
||||
elseif( empty( $data['children'] ) and !empty( $data['widgets'] ) )
|
||||
{
|
||||
/* If we have no children, and we have widgets at the root, move them one level down,
|
||||
with each widget as a separate child. */
|
||||
$data['children'] = [];
|
||||
foreach( $data['widgets'] as $key => $widget )
|
||||
{
|
||||
$this->addWidget( $widget );
|
||||
$data['children'][] = [
|
||||
'otherOptions' => $data['otherOptions'],
|
||||
'widgets' => [ $key => $widget ]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,6 +385,35 @@ class Area
|
||||
return !$this->isRoot() and !$this->isWidget();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this area uses no box styles
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function noBoxStyles() : bool
|
||||
{
|
||||
if ( !$this->isWidget() )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( empty( $this->options['noBox'] ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
foreach ( $this->widgets as $widgetData )
|
||||
{
|
||||
$widget = is_array( $widgetData ) ? Widget::createWidgetFromStoredData( $widgetData ) : null;
|
||||
return $widget instanceof Widget and $widget->allowNoBox;
|
||||
}
|
||||
}
|
||||
catch ( \Exception $e ) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether we should treat this area as an "area widget". This means that it's a wrapper for 1 widget and the widget supports different layouts
|
||||
*
|
||||
@@ -454,6 +498,7 @@ class Area
|
||||
{
|
||||
$classList[] = 'cWidgetContainer--noImages';
|
||||
}
|
||||
|
||||
/* Grab the configuration from the first widget in this area.
|
||||
There should be only one widget at this point.
|
||||
We use this to get the responsive classes so that we can show//hide
|
||||
@@ -469,6 +514,13 @@ class Area
|
||||
$classList[] = 'ipsResponsive_hide' . $device;
|
||||
}
|
||||
}
|
||||
|
||||
/* Are we removing the widget */
|
||||
if ( $this->noBoxStyles() )
|
||||
{
|
||||
$classList[] = 'cWidgetContainer--nobox';
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -476,6 +528,37 @@ class Area
|
||||
return $classList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return any CSS classes that should be applied on the widget level
|
||||
*
|
||||
* @param Widget $widget
|
||||
* @return array
|
||||
*/
|
||||
public function widgetClasses( Widget $widget ) : array
|
||||
{
|
||||
if( !$this->isWidget() )
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/* Get anything configured on the widget level.
|
||||
3rd party devs may wish to add classes based on their own logic. */
|
||||
$classList = $widget->getWrapperClasses();
|
||||
|
||||
if( !( $widget instanceof DatabaseWidget ) )
|
||||
{
|
||||
$classList[] = 'ipsWidget--' . $this->orientation();
|
||||
}
|
||||
|
||||
/* Additional classes based on area attributes */
|
||||
if( $this->noBoxStyles() )
|
||||
{
|
||||
$classList[] = 'ipsWidget--transparent';
|
||||
}
|
||||
|
||||
return array_unique( $classList );
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
@@ -559,42 +642,26 @@ class Area
|
||||
{
|
||||
$obj = new static( [], $id );
|
||||
|
||||
/* If this is a reaserved area, we don't allow nesting, so put the widgets straight in here */
|
||||
if( in_array( $id, static::$reservedAreas ) )
|
||||
$child = new static( [
|
||||
'otherOptions' => [
|
||||
'gapSize' => 20
|
||||
]
|
||||
], $id, $obj );
|
||||
|
||||
/* Set any custom options passed in */
|
||||
foreach( $options as $option => $value )
|
||||
{
|
||||
foreach( $options as $option => $value )
|
||||
{
|
||||
$obj->setOption( $option, $value );
|
||||
}
|
||||
|
||||
foreach( $widgets as $widget )
|
||||
{
|
||||
$obj->addWidget( $widget );
|
||||
}
|
||||
$child->setOption( $option, $value );
|
||||
}
|
||||
else
|
||||
|
||||
/* Add them individually so that they get cleaned up and defined properly */
|
||||
foreach( $widgets as $widget )
|
||||
{
|
||||
$child = new static( [
|
||||
'otherOptions' => [
|
||||
'gapSize' => 20
|
||||
]
|
||||
], $id, $obj );
|
||||
|
||||
/* Set any custom options passed in */
|
||||
foreach( $options as $option => $value )
|
||||
{
|
||||
$child->setOption( $option, $value );
|
||||
}
|
||||
|
||||
/* Add them individually so that they get cleaned up and defined properly */
|
||||
foreach( $widgets as $widget )
|
||||
{
|
||||
$child->addWidget( $widget );
|
||||
}
|
||||
|
||||
$obj->addChild( $child );
|
||||
$child->addWidget( $widget );
|
||||
}
|
||||
|
||||
$obj->addChild( $child );
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
|
||||
@@ -118,6 +118,11 @@ abstract class Widget
|
||||
*/
|
||||
public bool $allowCustomPadding = false;
|
||||
|
||||
/**
|
||||
* @brief Show the no wrap option in the page builder
|
||||
*/
|
||||
public bool $allowNoBox = false;
|
||||
|
||||
/**
|
||||
* @brief Unique key for this widget
|
||||
*/
|
||||
@@ -422,6 +427,11 @@ abstract class Widget
|
||||
$return['widget-paddingallowed'] = true;
|
||||
}
|
||||
|
||||
if( $this->allowNoBox )
|
||||
{
|
||||
$return['widget-noboxallowed'] = true;
|
||||
}
|
||||
|
||||
if( count( static::$disallowedAreas ) )
|
||||
{
|
||||
$return['widget-disallowed-areas'] = implode( ",", static::$disallowedAreas );
|
||||
|
||||
Reference in new issue
Block a user