Version 4.3.0

This commit is contained in:
Neo committed 2025-12-19 05:47:12 -08:00
1 parent 96997ddd8e
commit fe1acf984a
1349 files changed
+116159 -67711

No files matched your search

+33 -19
View File
@@ -6,7 +6,7 @@ has been used throughout Invision Community to add style, flair and functionalit
to our product.
NULLING
Last Updated: 25 July 2017
Last Updated: 18 April 2018
Author: IPBMafia.ru
AES implementation in PHP
@@ -22,17 +22,10 @@ BMDoHyeon font package
Website: http://font.woowahan.com/dohyeon/
License: Open Font License
Buzz.js
Description: HTML5 audio support for notifications
Location: applications/core/interface/buzz
Included version: 1.2
Website: http://buzz.jaysalvat.com/
License: http://opensource.org/licenses/MIT
CKEditor
Description: Provides the editor functionality for submitting content.
Location: In development: applications/core/dev/ckeditor (custom build with additional plugins and some code changes)
Included version: 4.7.1
Included version: 4.9
Website: http://ckeditor.com
License: http://www.gnu.org/copyleft/lesser.html
@@ -56,6 +49,13 @@ Devices Icon Pack
Website: https://www.iconfinder.com/iconsets/devices-42
License: Free for commercial use
EmojiOne
Description: Emoji images
Location: Served from CDN
Included version: 3.1
Website: https://www.emojione.com
License: https://d1j8pt39hxlh3d.cloudfront.net/license-free.pdf
FontAwesome
Description: General icons for UI
Location: Font files in applications/core/interface/font and CSS in development in applications/core/dev/css/global/framework/fonts.css (mixed with other CSS and modified to use correct path to font files)
@@ -95,7 +95,14 @@ history.js
Included version: 1.8b2
Website: https://github.com/browserstate/history.js
License: https://github.com/browserstate/history.js#license
Howler
Description: HTML5 audio api
Location: applications/core/interface/howler
Included version: 2.0.9
Website: https://github.com/goldfire/howler.js
License: http://opensource.org/licenses/MIT
HTML5shiv
Description: Provides some HTML5 compatability for IE8
Location: applications/core/interface/html5shiv/html5shiv.js
@@ -106,7 +113,7 @@ HTML5shiv
HTMLPurifier
Description: Validates and cleans submitted HTML for content
Location: system/3rd_party/HTMLPurifier
Included version: 4.9.3
Included version: 4.10.0
Website: http://htmlpurifier.org
License: http://www.gnu.org/copyleft/lesser.html
@@ -307,7 +314,14 @@ success.mp3 Notification Sound by RCP Tones
Location: /applications/core/interface/sounds/success.mp3
Website: http://rcptones.com/dev_tones/
License: http://creativecommons.org/licenses/by/3.0/us/
Twemoji
Description: Emoji images
Location: Served from CDN
Included version: 2.3
Website: http://twitter.github.io/twemoji/
License: https://github.com/twitter/twemoji/blob/gh-pages/LICENSE
Underscore.js
Description: General-purpose functional JS library
Location: dev/js/library/underscore/
@@ -315,13 +329,6 @@ Underscore.js
Website: http://underscorejs.org
License: https://github.com/documentcloud/underscore/blob/master/LICENSE
Video.js
Description: HTML5 video player for Gallery
Location: applications/interface/videojs
Included version: 5.19.2
Website: http://www.videojs.com/
License: https://github.com/videojs/video.js/blob/master/LICENSE
Whoops
Description: Provides error handling for development mode
Location: dev/Whoops
@@ -341,4 +348,11 @@ XRegExp (with addons)
Location: In development: dev/js/library/xregexp/xregexp-all.js
Included Version: 3.2.0
Website: http://xregexp.com
License: http://mit-license.org
Hammer.js
Description: Provides touch and gesture javascript event support
Location: dev/js/library/hammer
Included Version: 2.0.8
Website: http://hammerjs.github.io
License: http://mit-license.org
+10 -2
View File
@@ -95,7 +95,8 @@ class IPSUtf8
*/
public static function errorHandler( $errno, $errstr, $errfile, $errline, $trace=NULL )
{
if ( in_array( $errno, array( E_NOTICE ) ) )
/* We don't care about these in production */
if ( in_array( $errno, array( E_WARNING, E_NOTICE, E_STRICT, E_DEPRECATED ) ) )
{
return;
}
@@ -246,4 +247,11 @@ class IPSUtf8
/* Init */
IPSUtf8::init();
/* Custom mb_ucfirst() function - eval'd so we can put into global namespace */
eval( '
function mb_ucfirst()
{
$text = func_get_arg( 0 );
return mb_strtoupper( mb_substr( $text, 0, 1 ) ) . mb_substr( $text, 1 );
}
');
+51 -22
View File
@@ -995,7 +995,7 @@ class Convert
{
foreach( $tableData['definition']['indexes'] as $key => $index )
{
if ( $index['type'] === 'unique' )
if ( $index['type'] === 'unique' OR $index['type'] === 'primary' )
{
/* Make sure none of the columns in this index are auto_increment */
foreach( $index['columns'] AS $k => $idxcol )
@@ -1030,7 +1030,9 @@ class Convert
/* Did not work - try the long way */
static::$utf->query( "RENAME TABLE `" . static::$db->prefix . $name . "` TO `" . static::$db->prefix . $name . "_temp`" );
static::log( $name . " has Unique Index - temp table created from main." );
static::$utf->query( "CREATE TABLE `" . static::$db->prefix . $name . "` LIKE `" . static::$db->prefix . $name . "_temp`" );
/* Previously we used a raw create table query to create a copy, however this does not check index lengths. Leaving this for reference. */
//static::$utf->query( "CREATE TABLE `" . static::$db->prefix . $name . "` LIKE `" . static::$db->prefix . $name . "_temp`" );
static::$db->createTable( $this->checkTable( $tableData['definition'] ) );
static::$utf->query( "ALTER TABLE `" . static::$db->prefix . $name . "` CONVERT TO CHARACTER SET " . $charset . " COLLATE " . $collation );
static::$utf->query( "ALTER TABLE `" . static::$db->prefix . $name . "` DEFAULT CHARACTER SET " . $charset . " COLLATE " . $collation );
static::log( $name . " created from temp table." );
@@ -1280,7 +1282,7 @@ class Convert
/* MB4? */
$sessionData = \IPSUtf8\Session::i()->json;
$length = 0;
$multiplier = ( ! empty( $sessionData['use_utf8mb4'] ) ) ? 4 : 3;
$multiplier = 4;
$needsFixing = array();
$maxLen = 1000;
@@ -1293,30 +1295,44 @@ class Convert
{
foreach( $definition['indexes'] as $key => $index )
{
$thisLength = null;
$hasText = false;
foreach( $index['columns'] as $i => $column )
{
if ( isset( $definition['columns'][ $column ] ) and ( ( ! empty( $definition['columns'][ $column ]['length'] ) or in_array( mb_strtolower( $definition['columns'][ $column ]['type'] ), array( 'longtext', 'mediumtext', 'text' ) ) ) ) )
$thisLength = ( isset( $index['length'][ $i ] ) ) ? $index['length'][ $i ] : ( ( (int) $definition['columns'][ $column ]['length'] or empty( $definition['columns'][ $column ]['length'] ) ) ? $definition['columns'][ $column ]['length'] : 250 );
$isText = in_array( mb_strtolower( $definition['columns'][ $column ]['type'] ), array( 'mediumtext', 'text' ) );
if ( $hasText === false and $isText === true )
{
$length += (int) ( $definition['columns'][ $column ]['length'] ) ? $definition['columns'][ $column ]['length'] : 250;
$hasText = true;
}
if ( isset( $definition['columns'][ $column ] ) and ( ( ! empty( $thisLength ) or $isText ) ) )
{
$length += $thisLength;
}
}
if ( $length * $multiplier > $maxLen )
if ( ( $length * $multiplier > $maxLen ) or $hasText )
{
foreach( $index['columns'] as $i => $column )
{
if ( isset( $definition['columns'][ $column ] ) and ( ( ! empty( $definition['columns'][ $column ]['length'] ) or in_array( mb_strtolower( $definition['columns'][ $column ]['type'] ), array( 'longtext', 'mediumtext', 'text' ) ) ) ) )
$thisLength = ( isset( $index['length'][ $i ] ) ) ? $index['length'][ $i ] : ( (int) $definition['columns'][ $column ]['length'] ? $definition['columns'][ $column ]['length'] : 250 );
if ( isset( $definition['columns'][ $column ] ) and ( ( ! empty( $thisLength ) or in_array( mb_strtolower( $definition['columns'][ $column ]['type'] ), array( 'mediumtext', 'text' ) ) ) ) )
{
/* Column name, column length, column type */
$needsFixing[ $key ][ $i ] = array( $column, ( (int) $definition['columns'][ $column ]['length'] ) ? $definition['columns'][ $column ]['length'] : 250, $definition['columns'][ $column ]['type'] );
$needsFixing[ $key ][ $i ] = array( $column, $thisLength, $definition['columns'][ $column ]['type'] );
}
}
}
$length = 0;
}
}
if ( count( $needsFixing ) )
{
foreach( $needsFixing as $key => $i )
@@ -1334,21 +1350,23 @@ class Convert
/* Check each column can be reduced by the amount we need reducing */
$debt = 0;
$reduceEachBy = ( ( 100 / $totalLength ) * $maxChars ) / 100;
$reduceEachBy = ( ( 100 / $totalLength ) * $maxChars) / 100;
/* Apply debt if we have any. We do not reduce integers */
foreach( $i as $x => $vals )
{
if ( array_key_exists( mb_strtolower( $vals[2] ), static::$numericCols ) AND static::$numericCols[ mb_strtolower( $vals[2] ) ] !== NULL )
if ( in_array( mb_strtoupper( $vals[2] ), array_keys( static::$numericCols ) ) )
{
/* Don't go by "display" space, go by storage space, which is static regardless of supplied value ... an INT(4) still uses 11 chars */
$debt += static::$numericCols[ mb_strtolower( $vals[2] ) ];
$debt += $vals[1];
}
}
/* Recalculate value to multiply index sub lengths with (subtracting debt) */
$reduceEachBy = ( ( 100 / ( $totalLength - $debt ) ) * ( $maxChars - $debt ) ) / 100;
if ( $debt < $totalLength )
{
$reduceEachBy = ( ( 100 / ($totalLength - $debt) ) * ( $maxChars - $debt ) ) / 100;
}
foreach( $i as $x => $vals )
{
/* No length? */
@@ -1357,9 +1375,16 @@ class Convert
$vals[1] = 250;
}
if ( array_key_exists( mb_strtolower( $vals[2] ), static::$numericCols ) AND static::$numericCols[ mb_strtolower( $vals[2] ) ] !== NULL )
if ( in_array( mb_strtoupper( $vals[2] ), array_keys( static::$numericCols ) ) )
{
/* Preserve col len */
/* Preserve col len where possible but if the column length is greater than subpart allowed, NULL the length
otherwise MySQL will complain as you cannot use subpart on non-string column. */
if ( $vals[1] > floor( $maxLen / $multiplier ) )
{
$vals[1] = NULL;
$i[ $x ] = $vals;
}
continue;
}
@@ -1370,9 +1395,13 @@ class Convert
foreach( $i as $x => $vals )
{
if ( $definition['columns'][ $definition['indexes'][ $key ]['columns'][ $x ] ]['length'] != $vals[1] )
if ( in_array( mb_strtolower( $vals[2] ), static::$convertCols ) AND $definition['columns'][ $definition['indexes'][ $key ]['columns'][ $x ] ]['length'] != $vals[1] )
{
$definition['indexes'][ $key ]['length'][ $x ] = $vals[1];
$definition['indexes'][ $key ]['length'][ $x ] = intval( $vals[1] );
}
else
{
$definition['indexes'][ $key ]['length'][ $x ] = NULL;
}
}
}
+1 -1
View File
@@ -74,7 +74,7 @@ abstract class Charset
}
}
self::$method = ucfirst( self::$method );
self::$method = mb_ucfirst( self::$method );
if ( ! isset( static::$multitons[ self::$method ] ) )
{
+1
View File
@@ -9,5 +9,6 @@
*/
define('READ_WRITE_SEPARATION', FALSE);
define('REPORT_EXCEPTIONS', TRUE);
require_once '../init.php';
\IPS\Dispatcher\Admin::i()->run();
+21 -21
View File
@@ -1,85 +1,85 @@
{
">:(": {
":classic_angry:": {
"image": "angry.png",
"image_2x": "angry@2x.png"
},
":D": {
":classic_biggrin:": {
"image": "biggrin.png",
"image_2x": "biggrin@2x.png"
},
"O.o": {
":classic_blink:": {
"image": "blink.png",
"image_2x": "blink@2x.png"
},
":$": {
":classic_blush:": {
"image": "blush.png",
"image_2x": "blush@2x.png"
},
"B|": {
":classic_cool:": {
"image": "cool.png",
"image_2x": "cool@2x.png"
},
"¬¬": {
":classic_dry:": {
"image": "dry.png",
"image_2x": "dry@2x.png"
},
"^_^": {
":classic_happy:": {
"image": "happy.png",
"image_2x": "happy@2x.png"
},
"o.O": {
":classic_huh:": {
"image": "huh.png",
"image_2x": "huh@2x.png"
},
"xD": {
":classic_laugh:": {
"image": "laugh.png",
"image_2x": "laugh@2x.png"
},
":|": {
":classic_mellow:": {
"image": "mellow.png",
"image_2x": "mellow@2x.png"
},
":o": {
":classic_ohmy:": {
"image": "ohmy.png",
"image_2x": "ohmy@2x.png"
},
":ph34r:": {
":classic_ninja:": {
"image": "ph34r.png",
"image_2x": "ph34r@2x.png"
},
"9_9": {
":classic_rolleyes:": {
"image": "rolleyes.gif",
"image_2x": "rolleyes@2x.png"
},
":(": {
":classic_sad:": {
"image": "sad.png",
"image_2x": "sad@2x.png"
},
"-_-": {
":classic_sleep:": {
"image": "sleep.png",
"image_2x": "sleep@2x.png"
},
":)": {
":classic_smile:": {
"image": "smile.png",
"image_2x": "smile@2x.png"
},
":P": {
":classic_tongue:": {
"image": "tongue.png",
"image_2x": "tongue@2x.png"
},
":/": {
":classic_unsure:": {
"image": "unsure.png",
"image_2x": "unsure@2x.png"
},
":S": {
":classic_wacko:": {
"image": "wacko.png",
"image_2x": "wacko@2x.png"
},
";)": {
":classic_wink:": {
"image": "wink.png",
"image_2x": "wink@2x.png"
},
":x": {
":classic_love:": {
"image": "wub.png",
"image_2x": "wub@2x.png"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 705 B

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 584 B

After

Width:  |  Height:  |  Size: 15 KiB

+1 -1
View File
@@ -1 +1 @@
=============================[NOTE]============================= This software is Nulled! Nulled by IPBMafia.ru Buy license at https://www.invisionpower.com/buy/self-hosted ================================================================ IPS 4.2.0 Release (102002) =============================[NOTE]============================= Invision Community End User License Agreement 1. Acceptance This software license agreement (the “Agreement”) is between you (either an individual or entity, hereinafter referred to as “You” or “Your”) and Invision Power Services, Inc. (“IPS”, “We”, “Our” or “Us”) and pertains to the Invision Community software, its components, features, enhancements, updates and modifications (collectively the “Software” or “Suite”) and any related services (“Services”) provided by IPS under this Agreement. By purchasing, downloading, accessing, installing, facilitating the installation or using the Software or Services (or allowing or authorizing any other person to do so) you are warranting that you have the authority provided by your state, country or jurisdiction to enter into a legally binding contract and that you have read and agree to be bound by the terms of this Agreement. You understand and acknowledge that this Agreement comprises the entire agreement between You and IPS. This Agreement supersedes any prior agreements, promises, representations or descriptions. If you do not agree with the terms and conditions set forth hereinafter, you may not purchase, download, access, install or use the Software or Services. This Agreement may only be modified in writing, signed by an officer of IPS and You. 2. License Scope and Grant In consideration for payment of all applicable license fees and subject to the terms and conditions of this Agreement, IPS grants you a revocable, limited and non-exclusive license to install and use the Software and Services on a single installation, accessible via one URL. Access to services, components, software updates and technical support require an active license. All rights in and to the Software and Services are reserved to the use and benefit of IPS and/or its licensors, successors and assigners. (a) Test Installation. You may install and test one additional instance of the Software for the purpose of development and testing. This installation must be and remain isolated and inaccessible to the general public at all times. 3. Restrictions Your license to use the Software and Services is subject to the following terms, conditions and restrictions on use: (a) You may alter and modify the Software’s source code for Your use, subject to this Agreement, however, you may not distribute, publish, resell, or otherwise share the Software or derivative works based on the Software without prior express written consent from IPS. (b) You may not utilize the Software or Services to engage in, facilitate, or otherwise allow others to engage in any activity that violates any law or regulation or the terms and conditions within this Agreement. (c) You are solely responsible for complying with all governmental regulations and policies. You agree to indemnify Us from any loss, action or damage arising from Your failure to use the Software in a manner inconsistent with applicable legislation. (d) You may not remove any copyright or proprietary notices on or within the software’s source code at any time. A copyright removal license may be purchased to remove the public display of the Software’s publicly displayed copyright notice. 4. Intellectual Property We have the sole and exclusive right to the Software which is being licensed, not sold, to you by IPS under the terms and conditions of this agreement. The Software is protected by copyright, trademark and other intellectual property laws. IPS reserves any rights not expressly granted herein. 5. Assignment You may not rent, lease, sell, share or authorize or temporarily assign your rights to the license or this Agreement to another individual or entity except as expressly permitted herein. (a) You may, after six months from the original purchase date, permanently transfer your licensing rights and interest in this Agreement to a third party, provided the license is active and in good standing. Any transfer and reassignment must occur via the mechanisms provided within the invisioncommunity.com client area and all applicable fees must be paid for the reassignment to be considered valid. (b) You agree to provide this Agreement in its entirety to the assignee prior to reassignment and the assignee must agree to be bound to the terms of this Agreement. (c) You acknowledge that account and non license specific materials and purchases, including, but not limited to: account credits and third party (“marketplace”) add-ons and purchases are non-transferrable under any circumstances. (d) We reserve the right to refuse or reject reassignment at our soleLine truncated
=============================[NOTE]============================= This software is Nulled! Nulled by IPBMafia.ru Buy license at https://www.invisionpower.com/buy/self-hosted ================================================================ IPS 4.3.0 Release (103005) =============================[NOTE]============================= Invision Community End User License Agreement 1. Acceptance This software license agreement (the “Agreement”) is between you (either an individual or entity, hereinafter referred to as “You” or “Your”) and Invision Power Services, Inc. (“IPS”, “We”, “Our” or “Us”) and pertains to the Invision Community software, its components, features, enhancements, updates and modifications (collectively the “Software” or “Suite”) and any related services (“Services”) provided by IPS under this Agreement. By purchasing, downloading, accessing, installing, facilitating the installation or using the Software or Services (or allowing or authorizing any other person to do so) you are warranting that you have the authority provided by your state, country or jurisdiction to enter into a legally binding contract and that you have read and agree to be bound by the terms of this Agreement. You understand and acknowledge that this Agreement comprises the entire agreement between You and IPS. This Agreement supersedes any prior agreements, promises, representations or descriptions. If you do not agree with the terms and conditions set forth hereinafter, you may not purchase, download, access, install or use the Software or Services. This Agreement may only be modified in writing, signed by an officer of IPS and You. 2. License Scope and Grant In consideration for payment of all applicable license fees and subject to the terms and conditions of this Agreement, IPS grants you a revocable, limited and non-exclusive license to install and use the Software and Services on a single installation, accessible via one URL. Access to services, components, software updates and technical support require an active license. All rights in and to the Software and Services are reserved to the use and benefit of IPS and/or its licensors, successors and assigners. (a) Test Installation. You may install and test one additional instance of the Software for the purpose of development and testing. This installation must be and remain isolated and inaccessible to the general public at all times. 3. Restrictions Your license to use the Software and Services is subject to the following terms, conditions and restrictions on use: (a) You may alter and modify the Software’s source code for Your use, subject to this Agreement, however, you may not distribute, publish, resell, or otherwise share the Software or derivative works based on the Software without prior express written consent from IPS. (b) You may not utilize the Software or Services to engage in, facilitate, or otherwise allow others to engage in any activity that violates any law or regulation or the terms and conditions within this Agreement. (c) You are solely responsible for complying with all governmental regulations and policies. You agree to indemnify Us from any loss, action or damage arising from Your failure to use the Software in a manner inconsistent with applicable legislation. (d) You may not remove any copyright or proprietary notices on or within the software’s source code at any time. A copyright removal license may be purchased to remove the public display of the Software’s publicly displayed copyright notice. 4. Intellectual Property We have the sole and exclusive right to the Software which is being licensed, not sold, to you by IPS under the terms and conditions of this agreement. The Software is protected by copyright, trademark and other intellectual property laws. IPS reserves any rights not expressly granted herein. 5. Assignment You may not rent, lease, sell, share or authorize or temporarily assign your rights to the license or this Agreement to another individual or entity except as expressly permitted herein. (a) You may, after six months from the original purchase date, permanently transfer your licensing rights and interest in this Agreement to a third party, provided the license is active and in good standing. Any transfer and reassignment must occur via the mechanisms provided within the invisioncommunity.com client area and all applicable fees must be paid for the reassignment to be considered valid. (b) You agree to provide this Agreement in its entirety to the assignee prior to reassignment and the assignee must agree to be bound to the terms of this Agreement. (c) You acknowledge that account and non license specific materials and purchases, including, but not limited to: account credits and third party (“marketplace”) add-ons and purchases are non-transferrable under any circumstances. (d) We reserve the right to refuse or reject reassignment at our soleLine truncated
+1 -1
View File
@@ -12,7 +12,7 @@
{{endforeach}}
{{if $uploadField}}
<input type="hidden" name="MAX_FILE_SIZE" value="{$uploadField}">
<input type="hidden" name="plupload" value="{expression="md5( uniqid() )"}">
<input type="hidden" name="plupload" value="{expression="md5( mt_rand() )"}">
{{endif}}
{{if $error}}
<div class="ipsMessage ipsMessage_error">
@@ -0,0 +1,25 @@
<ips:template parameters="$html, $title=NULL" />
<!DOCTYPE html>
<html lang="en">
<head>
<title>{$title}</title>
<link rel='stylesheet' href='css/reset.css' media='all'>
<link rel='stylesheet' href='css/fonts.css' media='all'>
<link rel='stylesheet' href='css/global.css' media='all'>
<link rel='stylesheet' href='css/layout.css' media='all'>
<link rel='stylesheet' href='css/messages.css' media='all'>
<link rel='stylesheet' href='css/misc.css' media='all'>
<link rel='stylesheet' href='css/forms.css' media='all'>
<link rel='stylesheet' href='css/typography.css' media='all'>
<link rel='stylesheet' href='css/buttons.css' media='all'>
<link rel='stylesheet' href='css/installer.css' media='all'>
<script type="text/javascript">
var IPS_TIMEOUT = 30;
</script>
<script src="js/jquery.min.js"></script>
<script src="js/multipleRedirect.js"></script>
</head>
<body class="ipsApp ipsApp_front ipsJS_none ipsClearfix ipsLayout_noBackground">
{$html|raw}
</body>
</html>
@@ -70,7 +70,7 @@
<a href='{expression="\IPS\Settings::i()->base_url"}' class='ipsButton ipsButton_large ipsButton_primary'>{lang="gotosuite"}</a>
<a href='{expression="rtrim( \IPS\Settings::i()->base_url, '/' ) . '/' . \IPS\CP_DIRECTORY"}' class='ipsButton ipsButton_large ipsButton_normal'>{lang="gotoacp"}</a>
<br><br>
<a href='http://ipbmafia.ru/files/file/1848-russkiy-yazyk-dlya-invision-community-42/' target="_blank" class='ipsButton ipsButton_large ipsButton_normal' style="background: red;">Russian lang pack</a>
<a href='https://ipbmafia.ru/files/file/1848-russkiy-yazyk-dlya-invision-community/' target="_blank" class='ipsButton ipsButton_large ipsButton_normal' style="background: red;">Russian lang pack</a>
<br><br>
<a href="{expression="\IPS\Http\Url::ips('docs/docs')"}" target="_blank">{lang="docs_link"}</a>
</div>
+1
View File
@@ -9,5 +9,6 @@
*/
define('READ_WRITE_SEPARATION', FALSE);
define('REPORT_EXCEPTIONS', TRUE);
require_once '../../init.php';
\IPS\Dispatcher\Setup::i()->setLocation('install')->run();
+11 -2
View File
@@ -19,6 +19,7 @@ $lang = array(
'generic_error' => "An unknown error occurred",
'installation_error' => "Installation Error",
'installer_locked' => "Installer Locked.",
'session_no_good' => "We were unable to start a PHP session. You will need to contact your host to adjust your PHP configuration before you can continue. The error reported was: %s",
'error' => "Error",
'error_title' => "Error",
'page_doesnt_exist' => "That page doesn't exist.",
@@ -68,6 +69,7 @@ $lang = array(
'requirements_suhosin_cookie_encrypt' => "PHP setting suhosin.cookie.encrypt is set to 1. This can cause problems in some areas such as the editor's auto save functionality. We recommended a value of 0 to disable it.",
'requirements_file_system' => "File System",
'requirements_file_writable' => "%s is writable",
'requirements_mysql_timeout' => "Your MySQL %s system variable is currently set to an extremely low value (%s). The default MySQL value is 28800. It is recommended to increase this MySQL system variable to at least 20 in order to prevent potential problems with queries timing out.",
'dir_does_not_exist' => "%s does not exist.",
'dir_is_not_writable' => "%s cannot be written to. Please adjust the permissions on it or contact your hosting provider for assistance.",
'file_storage_test_error_amazon' => "There appears to be a problem with your Amazon (%s) file storage settings which can cause problems with uploads.<br> After attempting to upload a file to the directory, the URL to the file is returning a HTTP %s error. Update your settings and then check and see if the problem has been resolved",
@@ -128,8 +130,8 @@ $lang = array(
'base_url' => "Site URL",
'err_db_exists' => "There is already an Invision Community installation in that database. Choose a different database or use a table prefix.",
'err_db_cant_create' => "The Database does not exist and cannot be created automatically. Please create the database manually or contact your hosting provider if you are unsure how to do this.",
'diagnostics_reporting' => "Send diagnostics data to IPS?",
'diagnostics_reporting_desc' => "This distribution is nulled and this setting does not work. Help Invision Community improve by automatically sending usage and diagnostic information. The data sent does not contain any private information about your users or your community.",
'diagnostics_reporting' => "Send usage and diagnostics data to IPS?",
'diagnostics_reporting_desc' => "This distribution is nulled and this setting does not work. Help Invision Community improve by automatically sending usage and diagnostic information including a list of features in use and basic statistics. The data sent does not contain any private information about your users or your community.",
'conf_global_error' => "",
@@ -186,5 +188,12 @@ $lang = array(
'session_check_fail' => "The installer uses PHP sessions to store data, however PHP sessions are currently not working correctly on your server. This is an issue you will need to contact your host about.",
'files-1' => "File could not be opened: %s",
'files-2' => "File does not exist: %s",
'files-4' => "File could not be copied: %s",
'files-5' => "File could not be moved: %s",
'files-3' => "File could not be saved: %s",
'files-6' => "Folder could not be created: %s",
'files-7' => "Response returned a 400 response code but no Amazon region was returned: %s",
);
+397
View File
@@ -0,0 +1,397 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Invision Community Update Extractor</title>
<style type='text/css'>@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-moz-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-ms-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:0 0}to{background-position:40px 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.ipsProgressBar{width:50%;margin:auto;height:20px;overflow:hidden;background:#9c9c9c;background:-moz-linear-gradient(top,rgba(156,156,156,1) 0,rgba(180,180,180,1) 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0,rgba(156,156,156,1)),color-stop(100%,rgba(180,180,180,1)));background:-webkit-linear-gradient(top,rgba(156,156,156,1) 0,rgba(180,180,180,1) 100%);background:-o-linear-gradient(top,rgba(156,156,156,1) 0,rgba(180,180,180,1) 100%);background:-ms-linear-gradient(top,rgba(156,156,156,1) 0,rgba(180,180,180,1) 100%);background:linear-gradient(to bottom,rgba(156,156,156,1) 0,rgba(180,180,180,1) 100%);border-radius:4px;box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.ipsProgressBar_animated .ipsProgressBar_progress{background-color:#5490c0;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(.25,rgba(255,255,255,.15)),color-stop(.25,transparent),color-stop(.5,transparent),color-stop(.5,rgba(255,255,255,.15)),color-stop(.75,rgba(255,255,255,.15)),color-stop(.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:40px 40px;-webkit-animation:progress-bar-stripes 2s linear infinite;-moz-animation:progress-bar-stripes 2s linear infinite;-ms-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.ipsProgressBar_progress{float:left;width:0;height:100%;font-size:12px;color:#fff;text-align:center;text-shadow:0 -1px 0 rgba(0,0,0,.25);background:#5490c0;position:relative;padding-left:6px}.ipsProgressBar_progress[data-progress]:after{position:absolute;right:5px;top:0;line-height:32px;color:#fff;content:attr(data-progress);display:block;font-weight:700}</style>
</head>
<body style="margin:0">
<?php
/**
* @brief Update Extractor
* @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 31 Jul 2017
*/
// This file deliberately exists outside of the framework
// and MUST NOT call init.php
@ini_set('display_errors', 'off');
@require( "../../system/3rd_party/pclzip/pclzip.lib.php" );
if ( file_exists( "../../constants.php" ) )
{
require "../../constants.php";
}
foreach ( array( 'FOLDER_PERMISSION_NO_WRITE' => 0755, 'FILE_PERMISSION_NO_WRITE' => 0644, 'TEMP_DIRECTORY' => sys_get_temp_dir() ) as $k => $v )
{
if ( !defined( $k ) )
{
define( $k, $v );
}
}
/**
* Extractor Class
*/
class Extractor
{
const NUMBER_PER_GO = 20;
/**
* @brief File
*/
private $file;
/**
* @brief Root Directory
*/
private $container;
/**
* @brief PclZip Object
*/
private $zip;
/**
* @brief Zip Contents
*/
private $contents;
/**
* @brief FTP Connection
*/
private $ftp;
/**
* @brief SSH Connection
*/
private $ssh;
/**
* @brief SFTP Connection
*/
private $sftp;
/**
* @brief SFTP Directory
*/
private $sftpDir;
/**
* Constructor
*
* @param string $file Zip File
* @param string $container Root directory
* @return void
*/
public function __construct( $file, $container )
{
if ( !$file or !$container )
{
throw new Exception;
}
$this->file = $file;
$this->container = $container . '/';
}
/**
* Security Check
*
* @param string $key Security key
* @return void
*/
public function securityCheck( $key )
{
require "../../conf_global.php";
return $key === md5( $INFO['board_start'] . $_GET['file'] . $INFO['sql_pass'] );
}
/**
* Establish FTP/SSH connection
*
* @param array $value FTP/SSH Credentials
* @return void
*/
public function connectToFtp( $value )
{
if ( $value['protocol'] == 'sftp' )
{
$this->ssh = ssh2_connect( $value['server'], $value['port'] );
if ( $this->ssh === FALSE )
{
throw new Exception;
}
if ( !@ssh2_auth_password( $this->ssh, $value['un'], $value['pw'] ) )
{
throw new Exception;
}
$this->sftp = @ssh2_sftp( $this->ssh );
if ( $this->sftp === FALSE )
{
throw new Exception;
}
if ( $value['path'] and !@ssh2_sftp_stat( $this->sftp, $value['path'] ) )
{
throw new Exception;
}
$this->sftpDir = ssh2_sftp_realpath( $this->sftp, $value['path'] ) . '/';
}
else
{
if ( $value['protocol'] == 'ssl_ftp' )
{
$this->ftp = @ftp_ssl_connect( $value['server'], $value['port'], 3 );
}
else
{
$this->ftp = @ftp_connect( $value['server'], $value['port'], 3 );
}
if ( $this->ftp === FALSE )
{
throw new Exception;
}
if ( !@ftp_login( $this->ftp, $value['un'], $value['pw'] ) )
{
throw new Exception;
}
if( ftp_nlist( $this->ftp, '.' ) === FALSE )
{
@ftp_pasv( $this->ftp, true );
}
if ( !@ftp_chdir( $this->ftp, $value['path'] ) )
{
throw new Exception;
}
}
}
/**
* Number of files
*
* @return int
*/
public function numberOfFiles()
{
$properties = $this->zip->properties();
return $properties['nb'];
}
/**
* Extract the files
*
* @param int $offset Offset
* @return int
*/
public function extract( $offset )
{
$this->zip = new PclZip( $this->file );
$this->contents = $this->zip->listContent();
if ( !$this->contents )
{
throw new Exception;
}
$done = 0;
for ( $i = 0; $i < self::NUMBER_PER_GO; $i++ )
{
$index = $offset + $i;
if ( isset( $this->contents[ $index ] ) )
{
$this->_extractFile( $index );
$done++;
}
else
{
return $done;
}
}
return $done;
}
/**
* Extract a particular file
*
* @param int $index File index in zip
* @return void
*/
private function _extractFile( $index )
{
/* Get the file */
$path = mb_substr( $this->contents[ $index ]['filename'], 0, mb_strlen( $this->container ) ) === $this->container ? mb_substr( $this->contents[ $index ]['filename'], mb_strlen( $this->container ) ) : $this->contents[ $index ]['filename'];
if ( !$path or mb_substr( $path, -1 ) === '/' or mb_substr( $path, 0, 1 ) === '.' or mb_substr( $path, 0, 9 ) === '__MACOSX/' )
{
return;
}
/* Create a directory if needed */
$dir = dirname( $path );
$directories = array( $dir );
while ( $dir != '.' )
{
$dir = dirname( $dir );
if ( $dir != '.' )
{
$directories[] = $dir;
}
}
foreach ( array_reverse( $directories ) as $dir )
{
if ( !is_dir( "../../{$dir}" ) )
{
if ( $this->sftp )
{
@ssh2_sftp_mkdir( $this->sftp, $this->sftpDir . $dir );
}
if ( $this->ftp )
{
@ftp_mkdir( $this->ftp, $dir );
}
else
{
@mkdir( "../../{$dir}", FOLDER_PERMISSION_NO_WRITE );
}
}
}
/* Write contents */
$contents = @$this->zip->extractByIndex( $index, \PCLZIP_OPT_EXTRACT_AS_STRING );
$contents = $contents[0]['content'];
if ( $this->sftp or $this->ftp )
{
$tmpFile = tempnam( TEMP_DIRECTORY, 'IPS' );
\file_put_contents( $tmpFile, $contents );
if ( $this->sftp )
{
if ( @ssh2_scp_send( $this->ssh, $tmpFile, $this->sftpDir . $path ) === FALSE )
{
throw new \Exception;
}
}
else
{
if ( @ftp_put( $this->ftp, $path, $tmpFile, FTP_BINARY ) === FALSE )
{
throw new \Exception;
}
}
@unlink( $tmpFile );
}
else
{
/* Determine if the file exists (we'll want to know this later) */
$fileExists = file_exists( "../../{$path}" );
$fh = @\fopen( "../../{$path}", 'w+' );
if ( $fh === FALSE )
{
$lastError = error_get_last();
throw new Exception( $lastError['message'] );
}
if ( @\fwrite( $fh, $contents ) === FALSE )
{
$lastError = error_get_last();
throw new Exception( $lastError['message'] );
}
else
{
/* If the file existed before we started, we should clear it from opcache if opcache is enabled */
if( $fileExists )
{
if ( function_exists( 'opcache_invalidate' ) )
{
@opcache_invalidate( "../../{$path}" );
}
}
/* Otherwise, we should set file permissions on the file if it's brand new in case server defaults to something odd */
else
{
@chmod( "../../{$path}", FILE_PERMISSION_NO_WRITE );
}
}
@\fclose( $fh );
}
}
}
/* ! Controller */
try
{
$extractor = new Extractor( isset( $_GET['file'] ) ? $_GET['file'] : NULL, isset( $_GET['container'] ) ? $_GET['container'] : '' );
/* Check this request came from the ACP */
if ( !$extractor->securityCheck( $_GET['key'] ) )
{
throw new Exception;
}
/* Establish an FTP connection if necessary */
if ( $_GET['ftp'] )
{
$extractor->connectToFtp( $_GET['ftp'] );
}
/* Extract a batch of files */
$i = isset( $_GET['i'] ) ? $_GET['i'] : 0;
$done = $extractor->extract( $i );
/* If we didn't extract anything, are we done? */
if ( !$done and $i >= $extractor->numberOfFiles() )
{
@unlink( $_GET['file'] );
if ( function_exists( 'opcache_reset' ) )
{
@opcache_reset();
}
echo <<<HTML
<div class="ipsProgressBar ipsProgressBar_animated">
<div class="ipsProgressBar_progress" style="width: 100%;"></div>
</div>
<script type='text/javascript'>parent.location = parent.location + '&check=1';</script><noscript><a href='index.php' target='_parent'>Click here to continue</a></noscript>
HTML;
}
/* Nope, redirect to the next batch */
else
{
$newI = $i + $extractor::NUMBER_PER_GO;
$percentComplete = round( 100 / $extractor->numberOfFiles() * $newI );
$properties = array(
'file' => $_GET['file'],
'container' => $_GET['container'],
'key' => $_GET['key'],
'ftp' => $_GET['ftp'],
'i' => $newI,
);
$url = "extract.php?" . http_build_query( $properties );
echo <<<HTML
<div class="ipsProgressBar ipsProgressBar_animated">
<div class="ipsProgressBar_progress" style="width: {$percentComplete}%;"></div>
</div>
<noscript><a href='{$url}' target='_parent'>Click here to continue</a></noscript>
<script type='text/javascript'>window.onload = function(){setTimeout(function(){window.location = '{$url}';}, 50);};</script>
HTML;
}
}
catch ( Exception $e )
{
echo "<script type='text/javascript'>parent.location = parent.location + '&fail=1';</script><noscript>An error occurred. Please visit the <a href='https://remoteservices.invisionpower.com/docs/client_area' target='_blank'>client area</a> to manually download the latest version. After uploading the files, <a href='index.php' target='_parent'>continue to the upgrader</a>.</noscript>";
exit;
}
?>
</body>
</html>
+29 -23
View File
@@ -1,30 +1,36 @@
<ips:template parameters="$forms, $error" />
<ips:template parameters="$error" />
{{if $error !== NULL}}
<div class='ipsMessage ipsMessage_error'>
{lang="$error"}
</div>
<br>
{{endif}}
<div class='ipsColumns ipsColumns_collapsePhone'>
<div class='ipsColumn ipsColumn_fluid ipsPad'>
{{foreach $forms as $k => $form}}
{{if $k === '_standard'}}
{$form|raw}
{{endif}}
{{endforeach}}
</div>
{{if count ( $forms ) > 1}}
<div class='ipsColumn ipsColumn_veryWide ipsPad'>
<h2 class='ipsType_sectionHead'>{lang='sign_in_faster'}</h2>
<p class='ipsType_normal ipsType_reset ipsType_light'>{lang='sign_in_connect'}</p>
<br>
{{foreach $forms as $k => $form}}
{{if $k !== '_standard'}}
{$form|raw}
{{endif}}
{{endforeach}}
</div>
{{endif}}
</div>
<form method="post">
<input type="hidden" name="{$id}_submitted" value="1">
<ul class='ipsForm ipsForm_vertical'>
<li class="ipsFieldRow ipsClearfix">
<label class="ipsFieldRow_label" for="auth">
{lang="auth"}
<span class="ipsFieldRow_required">{lang="required"}</span>
</label>
<div class="ipsFieldRow_content">
<input type="text" placeholder="{lang="email_address"}" name="auth" id="auth" {{if isset( \IPS\Request::i()->auth )}}value="{request="auth"}"{{endif}}>
</div>
</li>
<li class="ipsFieldRow ipsClearfix">
<label class="ipsFieldRow_label" for="password">
{lang="password"}
<span class="ipsFieldRow_required">{lang="required"}</span>
</label>
<div class="ipsFieldRow_content">
<input type="password" placeholder="{lang="password"}" name="password" id="password" {{if isset( \IPS\Request::i()->password )}}value="{request="password"}"{{endif}}>
</div>
</li>
<li class=''>
<button type='submit' class='ipsButton ipsButton_primary ipsButton_medium'>{lang="login"}</button>
&nbsp;&nbsp;&nbsp;
<a href="{url="upgrader_login_help" ips="true"}" target="_blank" class="ipsType_blendLinks">{lang="help"}</a>
</li>
</ul>
</form>
+1 -1
View File
@@ -12,7 +12,7 @@
{{endforeach}}
{{if $uploadField}}
<input type="hidden" name="MAX_FILE_SIZE" value="{$uploadField}">
<input type="hidden" name="plupload" value="{expression="md5( uniqid() )"}">
<input type="hidden" name="plupload" value="{expression="md5( mt_rand() )"}">
{{endif}}
{{if $error}}
<div class="ipsMessage ipsMessage_error">
@@ -24,7 +24,7 @@
<body class='ipsApp ipsApp_installer'>
<script>
$(document).ready(function(){
$('input[name="diagnostics_reporting_checkbox"]').attr("disabled","disabled");
$('input[name="101079_diagnostics_reporting_checkbox"]').attr("disabled","disabled");
});
</script>
<div id='elInstaller_wrap'>
@@ -77,7 +77,7 @@
<a href='{expression="\IPS\Settings::i()->base_url"}' class='ipsButton ipsButton_large ipsButton_primary'>{lang="go_to_suite"}</a>
<a href='{expression="rtrim( \IPS\Settings::i()->base_url, '/' ) . '/' . \IPS\CP_DIRECTORY"}' class='ipsButton ipsButton_large ipsButton_normal'>{lang="go_to_acp"}</a>
<br><br>
<a href='http://ipbmafia.ru/files/file/1848-russkiy-yazyk-dlya-invision-community-42/' target="_blank" class='ipsButton ipsButton_large ipsButton_normal' style="background: red;">Russian lang pack</a>
<a href='https://ipbmafia.ru/files/file/1848-russkiy-yazyk-dlya-invision-community/' target="_blank" class='ipsButton ipsButton_large ipsButton_normal' style="background: red;">Russian lang pack</a>
<br><br>
<a href="{expression="\IPS\Http\Url::ips('docs/docs')"}" target="_blank">{lang="suite_docs"}</a>
</div>
+23 -1
View File
@@ -1,6 +1,28 @@
<ips:template parameters="$requirements, $designerMode" />
<ips:template parameters="$requirements, $designerMode, $incorrectFiles" />
{{$canProceed = TRUE;}}
<section>
{{if $incorrectFiles !== NULL}}
{{if $incorrectFiles}}
{{$canProceed = FALSE;}}
<h2 class="ipsType_sectionHead">{lang="hc_files"}</h2>
<br><br>
<p class="ipsType_normal">{lang="hc_files_fail"}</p>
<br>
<a class="ipsButton ipsButton_primary" href="{url="client_area" ips="true"}" target="_blank">{lang="hc_files_fail2"}</a>
<br><br>
<p class="ipsType_normal">{lang="hc_files_fail3"}</p>
<ul class="ipsPad ipsType_large ipsList_checks">
{{foreach $incorrectFiles as $file}}
<li class="fail">{$file}</li>
{{endforeach}}
</ul>
{{else}}
<h2 class="ipsType_sectionHead">{lang="hc_files"}</h2>
<ul class="ipsPad ipsType_large ipsList_checks">
<li class="success">{lang="hc_files_ok"}</li>
</ul>
{{endif}}
{{endif}}
{{if $designerMode}}
<h2 class="ipsType_sectionHead">{lang="hc_designers_mode"}</h2>
<ul class="ipsPad ipsType_large ipsList_checks">
@@ -23,5 +23,4 @@
</section>
<div class='ipsPad_double ipsType_center'>
<a href='{url="controller=continueupgrade&key={$_SESSION['uniqueKey']}"}' class='ipsButton ipsButton_large ipsButton_primary ipsButton_positive'>{lang="continue_upgrade"}</a>
<a href='{url="controller=systemcheck&key={$_SESSION['uniqueKey']}&skippreviousupgrade=1"}' class='ipsButton ipsButton_large ipsButton_primary'>{lang="start_fresh_upgrade"}</a>
</div>
+2 -2
View File
@@ -8,12 +8,12 @@
{{if $trace[3]['function'] == 'installDatabaseUpdates'}}
<small>
{expression="\IPS\ROOT_PATH"}/applications/{$trace[4]['args'][0][1]}/setup/upg_{$trace[3]['args'][0]}/queries.json{{if isset($_SESSION['lastJsonIndex'])}} - query #{$_SESSION['lastJsonIndex']}{{endif}}<br>
<pre>{$exception->query}</pre>
<pre>{{if $exception->query}}{$exception->query}{{else}}{$trace[0]['args'][0]}{{endif}}</pre>
</small>
{{else}}
<small>
{$trace[1]['file']}::{$trace[1]['line']}<br>
<pre>{$exception->query}</pre>
<pre>{{if $exception->query}}{$exception->query}{{else}}{$trace[0]['args'][0]}{{endif}}</pre>
</small>
{{endif}}
{{else}}
+1
View File
@@ -9,6 +9,7 @@
*/
define('READ_WRITE_SEPARATION', FALSE);
define('REPORT_EXCEPTIONS', TRUE);
/* Prevent hooks from running during the upgrade */
define('RECOVERY_MODE', TRUE);
+32 -4
View File
@@ -37,6 +37,7 @@ $lang = array(
'create_conf_global' => "Please create a %s directory and make it writeable. If you are not sure how to do this, contact your hosting provider.",
'session_no_good' => "We were unable to start a PHP session. You will need to contact your host to adjust your PHP configuration before you can continue. The error reported was: %s",
'upgrade_group_not_exist' => "You do not have permission to run the upgrader because your group does not exist.",
'help' => "Help",
'sign_in_short' => "Sign In",
'auth' => "Display Name or Email Address",
@@ -46,7 +47,7 @@ $lang = array(
'username' => "Display Name",
'email_address' => "Email Address",
'username_or_email' => "Username or Email Address",
'login_err_no_account' => "The %s you entered does not belong to any account. Make sure that it is typed correctly.",
'login_err_no_account' => "The email address you entered does not belong to any account. Make sure that it is typed correctly.",
'login_err_bad_password' => "The password you entered is incorrect. Please try again (make sure your caps lock is off).",
'login_err_locked_unlock' => "Your account has been locked. Try again in {# [1:minute][?:minutes]}.",
'login_err_locked_nounlock' => "Your account has been locked. Try again later.",
@@ -106,6 +107,8 @@ $lang = array(
'requirements_mysql_utf8_info' => "%s is %s",
'requirements_file_system' => "File System",
'requirements_file_writable' => "%s is writable",
'requirements_mysql_timeout' => "Your MySQL %s system variable is currently set to an extremely low value (%s). The default MySQL value is 28800. It is recommended to increase this MySQL system variable to at least 20 in order to prevent potential problems with queries timing out.",
'dir_not_provided' => "No directory provided. Please check file storage configurations.",
'dir_does_not_exist' => "%s does not exist.",
'dir_is_not_writable' => "%s cannot be written to. Please adjust the permissions on it or contact your hosting provider for assistance.",
'file_storage_test_error_amazon' => "There appears to be a problem with your Amazon (%s) file storage settings which can cause problems with uploads.<br> After attempting to upload a file to the directory, the URL to the file is returning a HTTP %s error. Update your settings and then check and see if the problem has been resolved",
@@ -507,7 +510,7 @@ $lang = array(
/* ! 101000 revert globalTemplate */
'101000_globalTemplate_revert' => "Revert custom themes which contain out of date globalTemplate templates?",
'101000_globalTemplate_revert_desc' => "In Invision Community 4.1, the template 'utilitiesMenu' has been removed. This is used within the globalTemplate. After upgrading, you may see a message about the theme being out of date if you do not revert.<br><strong>Reverting templates will lose any customizations</strong>",
'101000_yes' => "Yes, revert losing customiztions",
'101000_yes' => "Yes, revert losing customizations",
'101000_no' => "No, I will manually adjust the templates after upgrading",
/* ! CMS 101000 Options */
@@ -522,11 +525,14 @@ $lang = array(
/* ! Core 101079 Diagnostic reporting option */
'101079_diagnostics_reporting' => "Send diagnostics data to IPS?",
'101079_diagnostics_reporting_desc' => "This distribution is nulled and this setting does not work. Help Invision Community improve by automatically sending usage and diagnostic information. The data sent does not contain any private information about your users or your community.",
'101079_diagnostics_reporting_desc' => "This distribution is nulled and this setting does not work. Help Invision Community improve by automatically sending diagnostic information. The data sent does not contain any private information about your users or your community.",
/* ! Core 102000 Letter photo option */
'options_letter_photos' => "Enable letter photos?",
'options_letter_photos_desc' => "Do you wish to start using automatic letter photos instead of the default theme profile photo for users who have not uploaded a profile photo?",
/* ! IPS Connect discontinued warning */
'ipsconnect' => "IPS Connect",
'no_apps_upgrade' => 'There are no applications available to upgrade',
'exit_upgrader' => "Exit upgrader",
@@ -535,7 +541,6 @@ $lang = array(
'convert_charset_info' => "Your community suite data or database configuration is saved in an encoding other than UTF-8. Before your site can be upgraded, we must convert the encoding to UTF-8.",
'start_upgrade' => "Start Upgrade",
'continue_upgrade' => "Continue Upgrade",
'start_fresh_upgrade' => "Start Fresh Upgrade",
'continue_not_set' => "Not set",
'unfinished_upgrade_info' => "You have an unfinished upgrade from %s",
'hc_designers_mode' => "Designers' Mode On",
@@ -556,5 +561,28 @@ $lang = array(
'start_info' => "This process will install your software for you. Be sure you have your license key and MySQL database details to hand.",
'install_docs' => "Install Guide",
'js_confirm_mrerror' => 'The server encountered multiple instances where it has stopped responding.\nPress \'OK\' to reload this page and re-run this installation step.',
'hc_files' => "File Check",
'hc_files_ok' => "Source files are correct for the latest version",
'hc_files_fail' => "Some source files are not correct for the latest version. If you uploaded the files to your server manually, the process may have failed or not completed yet, or a new version may have been released since you downloaded. Download latest version from your client area and upload the files again in order to continue.",
'hc_files_fail2' => "Go To Client Area",
'hc_files_fail3' => "If you are sure the files have been uploaded correctly, make sure the permissions are set correctly on them.",
/* ! Core - 4.3 - Removing HTTPS settings */
'https_settings_changes' => "HTTPS Settings Changes",
'103000_usage_reporting' => "Send usage data to IPS?",
'103000_usage_reporting_desc' => "Help Invision Community improve by automatically sending usage information including a list of features in use and basic statistics. The data sent does not contain any private information about your users or your community.",
'admincp_hide_deprecate' => "Hide AdminCP link",
'recaptcha_v1_removed' => "CAPTCHA Settings",
'103000_ipsconnect_url' => "IPS Connect Master Site URL",
'103000_ipsconnect_client_id' => "IPS Connect Client Identifier",
'103000_ipsconnect_client_secret' => "IPS Connect Client Secret",
'files-1' => "File could not be opened: %s",
'files-2' => "File does not exist: %s",
'files-4' => "File could not be copied: %s",
'files-5' => "File could not be moved: %s",
'files-3' => "File could not be saved: %s",
'files-6' => "Folder could not be created: %s",
'files-7' => "Response returned a 400 response code but no Amazon region was returned: %s",
);
+31 -3
View File
@@ -33,12 +33,14 @@ class _blogs extends \IPS\Node\Api\NodeController
* GET /blog/blogs
* Get list of blogs
*
* @note For requests using an OAuth Access Token for a particular member, only blogs that are not disabled and not belonging to a particular club or social group will be included
* @apiparam string owners Comma-delimited list of member IDs - if provided, only blogs owned by those members are returned (can be used in conjection with groups or set to 0 to exclude member-created blogs)
* @apiparam string groups Comma-delimited list of group IDs - if provided, only blogs owned by those groups are returned (can be used in conjection with members or set to 0 to exclude group blogs)
* @apiparam int pinned If 1, only blogs which are pinned are returned, if 0 only not pinned
* @apiparam string sortBy What to sort by. Can be 'count_entries' for number of entries, 'last_edate' for last entry date or do not specify for ID
* @apiparam string sortDir Sort direction. Can be 'asc' or 'desc' - defaults to 'asc'
* @apiparam int page Page number
* @apiparam int perPage Number of results per page - defaults to 25
* @return \IPS\Api\PaginatedResponse<IPS\blog\Entry>
*/
public function GETindex()
@@ -65,6 +67,12 @@ class _blogs extends \IPS\Node\Api\NodeController
{
$where[] = array( 'blog_pinned=?', intval( \IPS\Request::i()->pinned ) );
}
/* Permission */
if ( $this->member )
{
$where[] = array( 'blog_disabled=0 AND blog_social_group IS NULL AND blog_club_id IS NULL' );
}
/* Sort */
if ( isset( \IPS\Request::i()->sortBy ) and in_array( \IPS\Request::i()->sortBy, array( 'count_entries', 'last_edate' ) ) )
@@ -83,7 +91,9 @@ class _blogs extends \IPS\Node\Api\NodeController
\IPS\Db::i()->select( '*', 'blog_blogs', $where, "{$sortBy} {$sortDir}" ),
isset( \IPS\Request::i()->page ) ? \IPS\Request::i()->page : 1,
'IPS\blog\Blog',
\IPS\Db::i()->select( 'COUNT(*)', 'blog_blogs', $where )->first()
\IPS\Db::i()->select( 'COUNT(*)', 'blog_blogs', $where )->first(),
$this->member,
isset( \IPS\Request::i()->perPage ) ? \IPS\Request::i()->perPage : NULL
);
}
@@ -92,7 +102,7 @@ class _blogs extends \IPS\Node\Api\NodeController
* Get information about a specific blog
*
* @param int $id ID Number
* @throws 2B302/1 INVALID_ID The blog ID does not exist
* @throws 2B302/1 INVALID_ID The blog ID does not exist or the authorized user does not have permission to view it
* @return \IPS\blog\Blog
*/
public function GETitem( $id )
@@ -106,9 +116,27 @@ class _blogs extends \IPS\Node\Api\NodeController
*
* @param int $id ID Number
* @return void
* @throws 2B302/2 INVALID_ID The blog ID does not exist or the authorized user does not have permission to view it
* @throws 2B302/3 NO_PERMISSION The authorized user does not have permission to delete the blog
*/
public function DELETEitem( $id )
{
return $this->_delete( $id );
$class = $this->class;
try
{
$blog = $this->member ? $class::loadAndCheckPerms( $id ) : $class::load( $id );
if ( !$blog->canDelete( $this->member ) )
{
throw new \IPS\Api\Exception( 'INVALID_ID', '2B302/3', 404 );
}
$blog->delete();
return new \IPS\Api\Response( 200, NULL );
}
catch ( \OutOfRangeException $e )
{
throw new \IPS\Api\Exception( 'INVALID_ID', '2B302/2', 404 );
}
}
}
+67 -22
View File
@@ -30,18 +30,19 @@ class _comments extends \IPS\Content\Api\CommentController
/**
* GET /blog/comments
* Get list of comments
*
* @apiparam string blogs Comma-delimited list of calendar IDs
* @note For requests using an OAuth Access Token for a particular member, only comments the authorized user can view will be included, and all comments on draft entries are excluded
* @apiparam string blogs Comma-delimited list of blog IDs
* @apiparam string authors Comma-delimited list of member IDs - if provided, only topics started by those members are returned
* @apiparam int locked If 1, only comments from entries which are locked are returned, if 0 only unlocked
* @apiparam int hidden If 1, only comments which are hidden are returned, if 0 only not hidden
* @apiparam int pinned If 1, only posts from topics which are pinned are returned, if 0 only not pinned
* @apiparam int featured If 1, only comments from entries which are featured are returned, if 0 only not featured
* @apiparam int draft If 1, only draft entries are returned, if 0 only published
* @apiparam int draft If 1, only draft entries are returned, if 0 only published. Ignored for requests using an OAuth Access Token for a particular member
* @apiparam string sortBy What to sort by. Can be 'date', 'title' or leave unspecified for ID
* @apiparam string sortDir Sort direction. Can be 'asc' or 'desc' - defaults to 'asc'
* @apiparam int page Page number
* @apiparam int perPage Number of results per page - defaults to 25
* @return \IPS\Api\PaginatedResponse<IPS\blog\Entry\Comment>
*/
public function GETindex()
@@ -50,7 +51,11 @@ class _comments extends \IPS\Content\Api\CommentController
$where = array();
/* Draft */
if ( isset( \IPS\Request::i()->draft ) )
if ( $this->member )
{
$where[] = array( 'entry_status=?', 'published' );
}
elseif ( isset( \IPS\Request::i()->draft ) )
{
$where[] = array( 'entry_status=?', \IPS\Request::i()->draft ? 'draft' : 'published' );
}
@@ -64,14 +69,24 @@ class _comments extends \IPS\Content\Api\CommentController
* View information about a specific comment
*
* @param int $id ID Number
* @throws 2B301/1 INVALID_ID The comment ID does not exist
* @throws 2B301/1 INVALID_ID The comment ID does not exist or the authorized user does not have permission to view it
* @return \IPS\blog\Entry\Comment
*/
public function GETitem( $id )
{
try
{
return new \IPS\Api\Response( 200, \IPS\blog\Entry\Comment::load( $id )->apiOutput() );
$class = $this->class;
if ( $this->member )
{
$object = $class::loadAndCheckPerms( $id, $this->member );
}
else
{
$object = $class::load( $id );
}
return new \IPS\Api\Response( 200, $object->apiOutput( $this->member ) );
}
catch ( \OutOfRangeException $e )
{
@@ -83,21 +98,23 @@ class _comments extends \IPS\Content\Api\CommentController
* POST /blog/comments
* Create a comment
*
* @note For requests using an OAuth Access Token for a particular member, any parameters the user doesn't have permission to use are ignored (for example, hidden will only be honoured if the authentictaed user has permission to hide content).
* @reqapiparam int entry The ID number of the entry the comment is for
* @reqapiparam int author The ID number of the member making the comment (0 for guest)
* @reqapiparam int author The ID number of the member making the comment (0 for guest). Required for requests made using an API Key or the Client Credentials Grant Type. For requests using an OAuth Access Token for a particular member, that member will always be the author
* @apiparam string author_name If author is 0, the guest name that should be used
* @reqapiparam string content The comment content as HTML (e.g. "<p>This is a comment.</p>")
* @apiparam datetime date The date/time that should be used for the comment date. If not provided, will use the current date/time
* @apiparam string ip_address The IP address that should be stored for the comment. If not provided, will use the IP address from the API request
* @reqapiparam string content The comment content as HTML (e.g. "<p>This is a comment.</p>"). Will be sanatized for requests using an OAuth Access Token for a particular member; will be saved unaltered for requests made using an API Key or the Client Credentials Grant Type.
* @apiparam datetime date The date/time that should be used for the comment date. If not provided, will use the current date/time. Ignored for requests using an OAuth Access Token for a particular member
* @apiparam string ip_address The IP address that should be stored for the comment. If not provided, will use the IP address from the API request. Ignored for requests using an OAuth Access Token for a particular member
* @apiparam int hidden 0 = unhidden; 1 = hidden, pending moderator approval; -1 = hidden (as if hidden by a moderator)
* @throws 2B301/2 INVALID_ID The comment ID does not exist
* @throws 1B301/3 NO_AUTHOR The author ID does not exist
* @throws 1B301/4 NO_CONTENT No content was supplied
* @throws 2B301/8 NO_PERMISSION The authorized user does not have permission to comment on that blog entry
* @return \IPS\blog\Entry\Comment
*/
public function POSTindex()
{
/* Get topic */
/* Get entry */
try
{
$entry = \IPS\blog\Entry::load( \IPS\Request::i()->entry );
@@ -108,18 +125,29 @@ class _comments extends \IPS\Content\Api\CommentController
}
/* Get author */
if ( \IPS\Request::i()->author )
if ( $this->member )
{
$author = \IPS\Member::load( \IPS\Request::i()->author );
if ( !$author->member_id )
if ( !$entry->canComment( $this->member ) )
{
throw new \IPS\Api\Exception( 'NO_AUTHOR', '1B301/3', 404 );
throw new \IPS\Api\Exception( 'NO_PERMISSION', '2B301/8', 403 );
}
$author = $this->member;
}
else
{
$author = new \IPS\Member;
$author->name = \IPS\Request::i()->author_name;
if ( \IPS\Request::i()->author )
{
$author = \IPS\Member::load( \IPS\Request::i()->author );
if ( !$author->member_id )
{
throw new \IPS\Api\Exception( 'NO_AUTHOR', '1B301/3', 404 );
}
}
else
{
$author = new \IPS\Member;
$author->name = \IPS\Request::i()->author_name;
}
}
/* Check we have a post */
@@ -136,13 +164,15 @@ class _comments extends \IPS\Content\Api\CommentController
* POST /blog/comments/{id}
* Edit a comment
*
* @note For requests using an OAuth Access Token for a particular member, any parameters the user doesn't have permission to use are ignored (for example, hidden will only be honoured if the authentictaed user has permission to hide content).
* @param int $id ID Number
* @apiparam int author The ID number of the member making the comment (0 for guest)
* @apiparam int author The ID number of the member making the comment (0 for guest). Ignored for requests using an OAuth Access Token for a particular member.
* @apiparam string author_name If author is 0, the guest name that should be used
* @apiparam string content The comment content as HTML (e.g. "<p>This is a comment.</p>")
* @apiparam string content The comment content as HTML (e.g. "<p>This is a comment.</p>"). Will be sanatized for requests using an OAuth Access Token for a particular member; will be saved unaltered for requests made using an API Key or the Client Credentials Grant Type.
* @apiparam int hidden 1/0 indicating if the topic should be hidden
* @throws 2B301/5 INVALID_ID The comment ID does not exist
* @throws 2B301/5 INVALID_ID The comment ID does not exist or the authorized user does not have permission to view it
* @throws 1B301/6 NO_AUTHOR The author ID does not exist
* @throws 2B301/9 NO_PERMISSION The authorized user does not have permission to edit the comment
* @return \IPS\blog\Entry\Comment
*/
public function POSTitem( $id )
@@ -151,6 +181,14 @@ class _comments extends \IPS\Content\Api\CommentController
{
/* Load */
$comment = \IPS\blog\Entry\Comment::load( $id );
if ( $this->member and !$comment->canView( $this->member ) )
{
throw new \OutOfRangeException;
}
if ( $this->member and !$comment->canEdit( $this->member ) )
{
throw new \IPS\Api\Exception( 'NO_PERMISSION', '2B301/9', 403 );
}
/* Do it */
try
@@ -173,14 +211,21 @@ class _comments extends \IPS\Content\Api\CommentController
* Deletes a comment
*
* @param int $id ID Number
* @throws 2B301/7 INVALID_ID The comment ID does not exist
* @throws 2B301/7 INVALID_ID The comment ID does not exist
* @throws 2B301/A NO_PERMISSION The authorized user does not have permission to delete the comment
* @return void
*/
public function DELETEitem( $id )
{
try
{
\IPS\blog\Entry\Comment::load( $id )->delete();
$class = $this->class;
$object = $class::load( $id );
if ( $this->member and !$object->canDelete( $this->member ) )
{
throw new \IPS\Api\Exception( 'NO_PERMISSION', '2B301/A', 403 );
}
$object->delete();
return new \IPS\Api\Response( 200, NULL );
}
+76 -28
View File
@@ -32,6 +32,7 @@ class _entries extends \IPS\Content\Api\ItemController
* GET /blog/entries
* Get list of entries
*
* @note For requests using an OAuth Access Token for a particular member, only entries that are published and in blogs that are not disabled and not belonging to a particular club or social group will be included
* @apiparam string blogs Comma-delimited list of blog IDs
* @apiparam string authors Comma-delimited list of member IDs - if provided, only entries started by those members are returned
* @apiparam int locked If 1, only entries which are locked are returned, if 0 only unlocked
@@ -42,6 +43,7 @@ class _entries extends \IPS\Content\Api\ItemController
* @apiparam string sortBy What to sort by. Can be 'date' for creation date, 'title' or leave unspecified for ID
* @apiparam string sortDir Sort direction. Can be 'asc' or 'desc' - defaults to 'asc'
* @apiparam int page Page number
* @apiparam int perPage Number of results per page - defaults to 25
* @return \IPS\Api\PaginatedResponse<IPS\blog\Entry>
*/
public function GETindex()
@@ -49,8 +51,12 @@ class _entries extends \IPS\Content\Api\ItemController
/* Where clause */
$where = array();
/* Draft */
if ( isset( \IPS\Request::i()->draft ) )
/* Permission */
if ( $this->member )
{
$where[] = array( 'entry_status=? AND blog_disabled=0 AND blog_social_group IS NULL AND blog_club_id IS NULL', 'published' );
}
elseif ( isset( \IPS\Request::i()->draft ) )
{
$where[] = array( 'entry_status=?', \IPS\Request::i()->draft ? 'draft' : 'published' );
}
@@ -85,7 +91,8 @@ class _entries extends \IPS\Content\Api\ItemController
*
* @param int $id ID Number
* @apiparam int page Page number
* @throws 2B300/1 INVALID_ID The entry ID does not exist
* @apiparam int perPage Number of results per page - defaults to 25
* @throws 2B300/1 INVALID_ID The entry ID does not exist or the authorized user does not have permission to view it
* @return \IPS\Api\PaginatedResponse<IPS\blog\Entry\Comment>
*/
public function GETitem_comments( $id )
@@ -104,15 +111,16 @@ class _entries extends \IPS\Content\Api\ItemController
* POST /blog/entries
* Create an entry
*
* @note For requests using an OAuth Access Token for a particular member, any parameters the user doesn't have permission to use are ignored (for example, locked will only be honoured if the authentictaed user has permission to lock topics).
* @reqapiparam int blog The ID number of the blog the entry should be created in
* @reqapiparam int author The ID number of the member creating the entry (0 for guest)
* @reqapiparam int author The ID number of the member creating the entry (0 for guest). Required for requests made using an API Key or the Client Credentials Grant Type. For requests using an OAuth Access Token for a particular member, that member will always be the author
* @reqapiparam string title The entry title
* @reqapiparam string entry The entry content as HTML (e.g. "<p>This is a blog entry.</p>")
* @reqapiparam string entry The entry content as HTML (e.g. "<p>This is a blog entry.</p>"). Will be sanatized for requests using an OAuth Access Token for a particular member; will be saved unaltered for requests made using an API Key or the Client Credentials Grant Type.
* @apiparam bool draft If this is a draft
* @apiparam string prefix Prefix tag
* @apiparam string tags Comma-separated list of tags (do not include prefix)
* @apiparam datetime date The date/time that should be used for the entry date. If not provided, will use the current date/time
* @apiparam string ip_address The IP address that should be stored for the entry/post. If not provided, will use the IP address from the API request
* @apiparam datetime date The date/time that should be used for the entry date. If not provided, will use the current date/time. Ignored for requests using an OAuth Access Token for a particular member
* @apiparam string ip_address The IP address that should be stored for the entry/post. If not provided, will use the IP address from the API request. Ignored for requests using an OAuth Access Token for a particular member
* @apiparam int locked 1/0 indicating if the entry should be locked
* @apiparam int hidden 0 = unhidden; 1 = hidden, pending moderator approval; -1 = hidden (as if hidden by a moderator)
* @apiparam int pinned 1/0 indicating if the entry should be pinned
@@ -121,6 +129,7 @@ class _entries extends \IPS\Content\Api\ItemController
* @throws 1B300/3 NO_AUTHOR The author ID does not exist
* @throws 1B300/4 NO_TITLE No title was supplied
* @throws 1B300/5 NO_CONTENT No content was supplied
* @throws 1B300/A NO_PERMISSION The authorized user does not have permission to create an entry in that blog
* @return \IPS\blog\Entry
*/
public function POSTindex()
@@ -136,17 +145,28 @@ class _entries extends \IPS\Content\Api\ItemController
}
/* Get author */
if ( \IPS\Request::i()->author )
if ( $this->member )
{
$author = \IPS\Member::load( \IPS\Request::i()->author );
if ( !$author->member_id )
if ( !$blog->can( 'add', $this->member ) )
{
throw new \IPS\Api\Exception( 'NO_AUTHOR', '1B300/3', 400 );
throw new \IPS\Api\Exception( 'NO_PERMISSION', '1B300/A', 403 );
}
$author = $this->member;
}
else
{
$author = new \IPS\Member;
if ( \IPS\Request::i()->author )
{
$author = \IPS\Member::load( \IPS\Request::i()->author );
if ( !$author->member_id )
{
throw new \IPS\Api\Exception( 'NO_AUTHOR', '1B300/3', 400 );
}
}
else
{
$author = new \IPS\Member;
}
}
/* Check we have a title and a description */
@@ -160,29 +180,31 @@ class _entries extends \IPS\Content\Api\ItemController
}
/* Do it */
return new \IPS\Api\Response( 201, $this->_create( $blog, $author )->apiOutput() );
return new \IPS\Api\Response( 201, $this->_create( $blog, $author )->apiOutput( $this->member ) );
}
/**
* POST /blog/entries/{id}
* Edit a blog entry
*
* @note For requests using an OAuth Access Token for a particular member, any parameters the user doesn't have permission to use are ignored (for example, locked will only be honoured if the authentictaed user has permission to lock topics).
* @reqapiparam int blog The ID number of the blog the entry should be created in
* @reqapiparam int author The ID number of the member creating the entry (0 for guest)
* @reqapiparam int author The ID number of the member creating the entry (0 for guest). Ignored for requests using an OAuth Access Token for a particular member.
* @reqapiparam string title The entry title
* @reqapiparam string entry The entry content as HTML (e.g. "<p>This is a blog entry.</p>")
* @reqapiparam string entry The entry content as HTML (e.g. "<p>This is a blog entry.</p>"). Will be sanatized for requests using an OAuth Access Token for a particular member; will be saved unaltered for requests made using an API Key or the Client Credentials Grant Type.
* @apiparam bool draft If this is a draft
* @apiparam string prefix Prefix tag
* @apiparam string tags Comma-separated list of tags (do not include prefix)
* @apiparam datetime date The date/time that should be used for the entry date. If not provided, will use the current date/time
* @apiparam string ip_address The IP address that should be stored for the entry/post. If not provided, will use the IP address from the API request
* @apiparam datetime date The date/time that should be used for the entry date. If not provided, will use the current date/time. Ignored for requests using an OAuth Access Token for a particular member.
* @apiparam string ip_address The IP address that should be stored for the entry/post. If not provided, will use the IP address from the API request. Ignored for requests using an OAuth Access Token for a particular member.
* @apiparam int locked 1/0 indicating if the entry should be locked
* @apiparam int hidden 0 = unhidden; 1 = hidden, pending moderator approval; -1 = hidden (as if hidden by a moderator)
* @apiparam int pinned 1/0 indicating if the entry should be pinned
* @apiparam int featured 1/0 indicating if the entry should be featured
* @throws 2B300/6 INVALID_ID The entry ID is invalid
* @throws 1B300/7 NO_BLOG The blog ID does not exist
* @throws 2B300/6 INVALID_ID The entry ID is invalid or the authorized user does not have permission to view it
* @throws 1B300/7 NO_BLOG The blog ID does not exist or the authorized user does not have permission to post in it
* @throws 1B300/8 NO_AUTHOR The author ID does not exist
* @throws 1B300/B NO_PERMISSION The authorized user does not have permission to edit that blog entry
* @return \IPS\blog\Entry
*/
public function POSTitem( $id )
@@ -190,13 +212,26 @@ class _entries extends \IPS\Content\Api\ItemController
try
{
$entry = \IPS\blog\Entry::load( $id );
if ( $this->member and !$entry->can( 'read', $this->member ) )
{
throw new \OutOfRangeException;
}
if ( $this->member and !$entry->canEdit( $this->member ) )
{
throw new \IPS\Api\Exception( 'NO_PERMISSION', '1B300/B', 403 );
}
/* New blog */
if ( isset( \IPS\Request::i()->blog ) and \IPS\Request::i()->blog != $entry->forum_id )
if ( isset( \IPS\Request::i()->blog ) and \IPS\Request::i()->blog != $entry->blog_id and ( !$this->member or $entry->canMove( $this->member ) ) )
{
try
{
$newBlog = \IPS\blog\Blog::load( \IPS\Request::i()->blog );
if ( $this->member and !$newBlog->can( 'add', $this->member ) )
{
throw new \OutOfRangeException;
}
$entry->move( $newBlog );
}
catch ( \OutOfRangeException $e )
@@ -206,7 +241,7 @@ class _entries extends \IPS\Content\Api\ItemController
}
/* New author */
if ( isset( \IPS\Request::i()->author ) )
if ( !$this->member and isset( \IPS\Request::i()->author ) )
{
try
{
@@ -225,11 +260,11 @@ class _entries extends \IPS\Content\Api\ItemController
}
/* Everything else */
$this->_createOrUpdate( $entry );
$this->_createOrUpdate( $entry, 'edit' );
/* Save and return */
$entry->save();
return new \IPS\Api\Response( 200, $entry->apiOutput() );
return new \IPS\Api\Response( 200, $entry->apiOutput( $this->member ) );
}
catch ( \OutOfRangeException $e )
{
@@ -241,9 +276,10 @@ class _entries extends \IPS\Content\Api\ItemController
* Create or update entry
*
* @param \IPS\Content\Item $item The item
* @param string $type add or edit
* @return \IPS\Content\Item
*/
protected function _createOrUpdate( \IPS\Content\Item $item )
protected function _createOrUpdate( \IPS\Content\Item $item, $type='add' )
{
/* Is draft */
if ( isset( \IPS\Request::i()->draft ) )
@@ -254,11 +290,16 @@ class _entries extends \IPS\Content\Api\ItemController
/* Content */
if ( isset( \IPS\Request::i()->entry ) )
{
$item->content = \IPS\Request::i()->entry;
$entryContents = \IPS\Request::i()->entry;
if ( $this->member )
{
$entryContents = \IPS\Text\Parser::parseStatic( $entryContents, TRUE, NULL, $this->member, 'blog_Entries' );
}
$item->content = $entryContents;
}
/* Pass up */
return parent::_createOrUpdate( $item );
return parent::_createOrUpdate( $item, $type );
}
/**
@@ -266,14 +307,21 @@ class _entries extends \IPS\Content\Api\ItemController
* Delete an entry
*
* @param int $id ID Number
* @throws 2B300/9 INVALID_ID The entry ID does not exist
* @throws 2B300/9 INVALID_ID The entry ID does not exist
* @throws 2B300/C NO_PERMISSION The authorized user does not have permission to delete the entry
* @return void
*/
public function DELETEitem( $id )
{
try
{
\IPS\blog\Entry::load( $id )->delete();
$item = \IPS\blog\Entry::load( $id );
if ( $this->member and !$item->canDelete( $this->member ) )
{
throw new \IPS\Api\Exception( 'NO_PERMISSION', '2B300/C', 404 );
}
$item->delete();
return new \IPS\Api\Response( 200, NULL );
}
+2 -2
View File
@@ -26,7 +26,7 @@
</p>
</td>
</tr>]]></template_content_html><template_data>$entry, $email</template_data><template_content_plaintext><![CDATA[{$email->language->addToStack("x_created_entry_in", FALSE, array( 'sprintf' => array( $entry->author()->name, $entry->container()->_title ) ) )}: {$entry->title} ({$entry->url()})
]]></template_content_plaintext></template><template><template_app>blog</template_app><template_name>digests__comment</template_name><template_content_html><![CDATA[<tr style='border-bottom: 1px solid #eeeeee'>
]]></template_content_plaintext><template_pinned>0</template_pinned></template><template><template_app>blog</template_app><template_name>digests__comment</template_name><template_content_html><![CDATA[<tr style='border-bottom: 1px solid #eeeeee'>
<td dir='{dir}' width='40' align='center' valign='top' class=' '>
<img src='{$comment->author()->get_photo( true, true )}' width='32' height='32' style='border: 1px solid #000; vertical-align: middle;'>
</td>
@@ -42,4 +42,4 @@
</div>
</td>
</tr>]]></template_content_html><template_data>$comment, $email</template_data><template_content_plaintext><![CDATA[{$email->language->addToStack("x_commented_entry", FALSE, array( 'sprintf' => array( $comment->author()->name ) ) )}: {$comment->item()->name} ({$comment->url()})
]]></template_content_plaintext></template></emails>
]]></template_content_plaintext><template_pinned>0</template_pinned></template></emails>
+38 -1
View File
@@ -1,6 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<javascript app="blog">
<file javascript_app="blog" javascript_location="front" javascript_path="controllers/browse" javascript_name="ips.browse.list.js" javascript_type="controller" javascript_version="102002" javascript_position="1000050">/**
<file javascript_app="blog" javascript_location="front" javascript_path="controllers/browse" javascript_name="ips.browse.grid.js" javascript_type="controller" javascript_version="103005" javascript_position="1000050"><![CDATA[/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
* ips.browse.list.js - Blog browse list controller
*
* Author: Rikki Tissier
*/
;( function($, _, undefined){
"use strict";
ips.controller.register('blog.front.browse.grid', {
initialize: function () {
this.on('click', '.cBlog_grid_item', this.clickNewsItem);
this.setup();
},
setup: function () {
},
clickNewsItem: function (e) {
var link = $( e.currentTarget ).find('[data-role="newsTitle"]');
if( $( e.target ).closest('a').length > 0 ){
return;
}
if( $( e.target ).closest('input').length > 0 ){
return;
}
window.location = link.attr('href');
}
});
}(jQuery, _));]]></file>
<file javascript_app="blog" javascript_location="front" javascript_path="controllers/browse" javascript_name="ips.browse.list.js" javascript_type="controller" javascript_version="103005" javascript_position="1000050">/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
+14 -3
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<language>
<app key="blog" version="102002">
<app key="blog" version="103005">
<word key="__app_blog" js="0">Blogs</word>
<word key="frontnavigation_blogs" js="0">Blogs</word>
<word key="__indefart_blog_entry_comment" js="0">a comment on a blog entry</word>
@@ -34,6 +34,13 @@
<word key="blog_allow_rssimport" js="0">Allow RSS imports</word>
<word key="blog_allow_rss" js="0">Allow RSS exports</word>
<word key="blog_allow_rss_desc" js="0"><![CDATA[Only content that is available to guests will be shown. For more options, create a <a href='{internal.app=core&module=discovery&controller=rss}'>Custom RSS Feed</a>]]></word>
<word key="blog_allow_grid" js="0">Enable grid view</word>
<word key="blog_allow_grid_desc" js="0">Grid view showcases blog entries as cards with space for a feature photo</word>
<word key="blog_view_mode" js="0">Default view</word>
<word key="blog_view_mode_grid" js="0">Grid of latest entries</word>
<word key="blog_view_mode_grid_desc" js="0">Displays entries as cards in a grid with space for a feature photo</word>
<word key="blog_view_mode_list" js="0">List of blogs</word>
<word key="blog_view_mode_list_desc" js="0">Displays blogs as a sortable and filterable list</word>
<word key="blogs" js="0">Blogs</word>
<word key="blog_word_custom" js="0">Blog Name</word>
<word key="blog_blog_member_id" js="0">Blog Author</word>
@@ -54,6 +61,8 @@
<word key="select_blog" js="0">Select Blog</word>
<word key="search_contextual_item_blogs" js="0">This Blog</word>
<word key="search_contextual_item_blog_entry" js="0">This Entry</word>
<word key="view_blogs_as_list" js="0">View Blogs</word>
<word key="view_blogs_as_grid" js="0">View Latest Entries</word>
<word key="blog_settings" js="0">Blog Settings</word>
<word key="blog_name" js="0">Name</word>
<word key="blog_name_group" js="0">Name</word>
@@ -80,7 +89,7 @@
<word key="submit_entry" js="0">Submit New Blog Entry</word>
<word key="blog_entry_title" js="0">Title</word>
<word key="blog_entry_tags" js="0">Tags</word>
<word key="blog_entry_auto_follow" js="0">Auto Follow</word>
<word key="blog_entry_auto_follow" js="0"></word>
<word key="blog_entry_auto_follow_suffix" js="0">Notify me when other comments are posted</word>
<word key="blog_entry_content" js="0">Content</word>
<word key="blog_entry_date" js="0">Publish Time</word>
@@ -157,6 +166,7 @@
<word key="unpin_blog" js="0">Unpin</word>
<word key="blog_by" js="0">A blog by %s</word>
<word key="group_blog_by" js="0">A group blog by %s</word>
<word key="club_blog_for" js="0">A club blog for %s</word>
<word key="contributors_to_this" js="0">Contributors to this blog</word>
<word key="num_contributors_to_this" js="0">{# [1:contributor][?:contributors]} to this blog</word>
<word key="about_this_blog" js="0">About this blog</word>
@@ -212,7 +222,7 @@
<word key="can_unhide_blog_entry" js="0">Can unhide entries?</word>
<word key="can_view_hidden_blog_entry" js="0">Can view hidden entries?</word>
<word key="can_move_blog_entry" js="0">Can move entries?</word>
<word key="can_move_blog_entry_desc" js="0">Will be able to move entries only between the blogs they have permission to moderate, not to any other blog.</word>
<word key="can_move_blog_entry_desc" js="0">Will be able to move entries only between the blogs they can create new content within.</word>
<word key="can_lock_blog_entry" js="0">Can lock entries?</word>
<word key="can_unlock_blog_entry" js="0">Can unlock entries?</word>
<word key="can_reply_to_locked_blog_entry" js="0">Can reply to locked entries?</word>
@@ -257,5 +267,6 @@
<word key="blog_sidebar_title" js="0">Blog Information</word>
<word key="blog_bm_filters" js="0">Blog</word>
<word key="mf_has_blog" js="0">Member has a Blog?</word>
<word key="blog_entry_cover_photo" js="0">Feature Photo</word>
</app>
</language>
+23 -4
View File
@@ -449,7 +449,9 @@
]
}
},
"collation": "utf8mb4_unicode_ci"
"collation": "utf8mb4_unicode_ci",
"comment": "",
"reporting": "count"
},
"blog_entries": {
"name": "blog_entries",
@@ -991,8 +993,22 @@
"length": [
null
]
},
"entry_author_id": {
"type": "key",
"name": "entry_author_id",
"columns": [
"entry_author_id",
"entry_last_update"
],
"length": [
null,
null
]
}
}
},
"comment": "",
"reporting": "count"
},
"blog_comments": {
"name": "blog_comments",
@@ -1219,7 +1235,9 @@
null
]
}
}
},
"comment": "",
"reporting": "count"
},
"blog_rss_import": {
"name": "blog_rss_import",
@@ -1395,7 +1413,8 @@
]
}
},
"comment": ""
"comment": "",
"reporting": "count"
},
"blog_rss_imported": {
"name": "blog_rss_imported",
+18 -4
View File
@@ -1,18 +1,32 @@
[
{
"key": "blog_allow_grid",
"default": "1",
"report": "full"
},
{
"key": "blog_allow_rss",
"default": "1"
"default": "1",
"report": "full"
},
{
"key": "blog_allow_rssimport",
"default": "1"
"default": "1",
"report": "full"
},
{
"key": "blog_enable_rating",
"default": "0"
"default": "0",
"report": "full"
},
{
"key": "blog_enable_sidebar",
"default": "0"
"default": "0",
"report": "full"
},
{
"key": "blog_view_mode",
"default": "grid",
"report": "bool"
}
]
File diff suppressed because it is too large. Load diff
+52 -1
View File
@@ -198,5 +198,56 @@
"101117": "4.2.0 Beta 8",
"102000": "4.2.0",
"102001": "4.2.0",
"102002": "4.2.0"
"102002": "4.2.0",
"102003": "4.2.0",
"102004": "4.2.1",
"102005": "4.2.2 Beta 1",
"102006": "4.2.2 Beta 2",
"102007": "4.2.2",
"102008": "4.2.2",
"102009": "4.2.2",
"102010": "4.2.3 Beta 1",
"102011": "4.2.3",
"102012": "4.2.3",
"102013": "4.2.3",
"102014": "4.2.4 Beta 1",
"102015": "4.2.4 Beta 2",
"102016": "4.2.4",
"102017": "4.2.4",
"102018": "4.2.4",
"102019": "4.2.4",
"102020": "4.2.4",
"102021": "4.2.5 Beta 1",
"102022": "4.2.5 Beta 2",
"102023": "4.2.5 Beta 3",
"102024": "4.2.5",
"102025": "4.2.5",
"102026": "4.2.5",
"102027": "4.2.5",
"102028": "4.2.5",
"102029": "4.2.5",
"102030": "4.2.6 Beta 1",
"102031": "4.2.6 Beta 1",
"102032": "4.2.6 Beta 2",
"102033": "4.2.6 Beta 3",
"102034": "4.2.6",
"102035": "4.2.6",
"102036": "4.2.6",
"102037": "4.2.7 Beta 1",
"102038": "4.2.7 Beta 2",
"102039": "4.2.7 Beta 3",
"102040": "4.2.7 Beta 4",
"102041": "4.2.7 Beta 5",
"102042": "4.2.7",
"102043": "4.2.7",
"102044": "4.2.7",
"102045": "4.2.7",
"102046": "4.2.7",
"102047": "4.2.8",
"102048": "4.2.8",
"103000": "4.3.0 Beta 1",
"103002": "4.3.0 Beta 2",
"103003": "4.3.0 Beta 3",
"103004": "4.3.0 Beta 4",
"103005": "4.3.0"
}
@@ -36,7 +36,7 @@ class _Blog
/**
* Constructor
*
* @param \IPS\Member|IPS\Member\Group|NULL $member If checking access, the member/group to check for, or NULL to not check access
* @param \IPS\Member|\IPS\Member\Group|NULL $member If checking access, the member/group to check for, or NULL to not check access
* @return void
*/
public function __construct( $member = NULL )
@@ -30,15 +30,31 @@ class _Entry
*/
public function getItems()
{
if ( \IPS\blog\Entry::canCreate( \IPS\Member::loggedIn() ) )
{
return array(
'blog_entry' => array(
'link' => \IPS\Http\Url::internal( "app=blog&module=blogs&controller=submit", 'front', 'blog_submit' ),
'extraData' => array( 'data-ipsDialog' => true, 'data-ipsDialog-size' => "narrow", ),
'title' => 'select_blog'
)
);
$blogs = \IPS\blog\Blog::loadByOwner( \IPS\Member::loggedIn(), array( array( 'blog_disabled=?', 0 ) ) );
if (
count( $blogs )
or
( \IPS\Settings::i()->club_nodes_in_apps and count( \IPS\blog\Blog::clubNodes( 'add' ) ) )
) {
if ( !\IPS\Settings::i()->club_nodes_in_apps AND count( $blogs ) === 1 )
{
$ourBlog = array_shift( $blogs );
return array(
'blog_entry' => array(
'link' => \IPS\Http\Url::internal( "app=blog&module=blogs&controller=submit&id=" . $ourBlog->_id, 'front', 'blog_submit' ),
)
);
}
else
{
return array(
'blog_entry' => array(
'link' => \IPS\Http\Url::internal( "app=blog&module=blogs&controller=submit", 'front', 'blog_submit' ),
'extraData' => array( 'data-ipsDialog' => true, 'data-ipsDialog-size' => "narrow", ),
'title' => 'select_blog'
)
);
}
}
else
{
@@ -53,7 +53,7 @@ class _Blogs
$blogs = \IPS\Db::i()->select(
"*",
'blog_blogs',
array( "blog_club_id IS NULL AND ( (blog_member_id>? AND blog_name LIKE CONCAT( '%', ?, '%' ) ) OR ( word_custom LIKE CONCAT( '%', ?, '%' ) AND lang_id=? ) )", 0, $searchTerm, $searchTerm, \IPS\Member::loggedIn()->language()->id ),
array( "blog_club_id IS NULL AND ( word_custom LIKE CONCAT( '%', ?, '%' ) AND lang_id=? )", $searchTerm, \IPS\Member::loggedIn()->language()->id ),
NULL,
array( 0, 500 )
)->join(
@@ -82,7 +82,7 @@ class _RebuildEntryImages
$file = \IPS\File::create( 'core_Attachment', $entry['entry_image'], NULL, NULL, FALSE, $filePath );
$attachment = $file->makeAttachment('', \IPS\Member::load( $entry['entry_author_id'] ) );
$fileName = htmlspecialchars( $attachment['attach_file'], \IPS\HTMLENTITIES, 'UTF-8', TRUE );
$fileName = htmlspecialchars( $attachment['attach_file'], ENT_DISALLOWED, 'UTF-8', TRUE );
$image = <<<IMAGE
<a href="<fileStore.core_Attachment>/{$file}" class="ipsAttachLink ipsAttachLink_image" style="float:left;"><img data-fileid="{$attachment['attach_id']}" src="<fileStore.core_Attachment>/{$file}" class="ipsImage ipsImage_thumbnailed" style="margin:10px;" alt="{$fileName}"></a>
@@ -47,7 +47,7 @@ class _blogs extends \IPS\Node\Controller
protected function manage()
{
/* Create the table */
$table = new \IPS\Helpers\Table\Db( 'blog_blogs', \IPS\Http\Url::internal( 'app=blog&module=blogs&controller=blogs' ) );
$table = new \IPS\Helpers\Table\Db( 'blog_blogs', \IPS\Http\Url::internal( 'app=blog&module=blogs&controller=blogs' ), array( array( 'blog_club_id IS NULL' ) ) );
$table->langPrefix = 'blog_';
/* Column stuff */
@@ -55,7 +55,7 @@ class _blogs extends \IPS\Node\Controller
$table->mainColumn = 'word_custom';
$table->joins = array(
array( 'select' => 'CASE WHEN blog_blogs.blog_member_id THEN blog_blogs.blog_name ELSE w.word_custom END as word_custom', 'from' => array( 'core_sys_lang_words', 'w' ), 'where' => "w.word_key=CONCAT( 'blogs_blog_', blog_blogs.blog_id ) AND w.lang_id=" . \IPS\Member::loggedIn()->language()->id )
array( 'select' => 'w.word_custom', 'from' => array( 'core_sys_lang_words', 'w' ), 'where' => "w.word_key=CONCAT( 'blogs_blog_', blog_blogs.blog_id ) AND w.lang_id=" . \IPS\Member::loggedIn()->language()->id )
);
/* Sort stuff */
@@ -72,11 +72,13 @@ class _blogs extends \IPS\Node\Controller
$table->parsers = array(
'blog_member_id' => function( $val, $row )
{
try
$member = \IPS\Member::load( $val );
if( $member->member_id )
{
return \IPS\Member::load( $val )->link();
return $member->link();
}
catch( \OutOfRangeException $e )
else
{
return NULL;
}
@@ -88,12 +90,7 @@ class _blogs extends \IPS\Node\Controller
return \IPS\Member::loggedIn()->language()->addToStack('blog_member_deleted');
}
if( $row['blog_member_id'] )
{
$val = htmlspecialchars( $val, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE );
}
return $val;
return htmlspecialchars( $val, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', FALSE );
}
);
@@ -45,6 +45,11 @@ class _settings extends \IPS\Dispatcher\Controller
$form->add( new \IPS\Helpers\Form\YesNo( 'blog_enable_rating', \IPS\Settings::i()->blog_enable_rating ) );
$form->add( new \IPS\Helpers\Form\YesNo( 'blog_enable_sidebar', \IPS\Settings::i()->blog_enable_sidebar ) );
$form->add( new \IPS\Helpers\Form\YesNo( 'blog_allow_grid', \IPS\Settings::i()->blog_allow_grid, FALSE, array( 'togglesOn' => array( 'blog_view_mode' ) ) ) );
$form->add( new \IPS\Helpers\Form\Radio( 'blog_view_mode', \IPS\Settings::i()->blog_view_mode, FALSE, array( 'options' => array(
'grid' => 'blog_view_mode_grid',
'list' => 'blog_view_mode_list'
) ), NULL, NULL, NULL, 'blog_view_mode' ) );
$form->addHeader('blog_settings_rss');
$form->add( new \IPS\Helpers\Form\YesNo( 'blog_allow_rssimport', \IPS\Settings::i()->blog_allow_rssimport ) );
@@ -31,26 +31,72 @@ class _browse extends \IPS\Dispatcher\Controller
protected function manage()
{
/* Featured stuff */
$featured = iterator_to_array( \IPS\blog\Entry::featured( 5, '_rand' ) );
/* Blogs table */
$table = new \IPS\blog\Blog\Table( \IPS\Http\Url::internal( 'app=blog&module=blogs&controller=browse', 'front', 'blogs' ) );
$table->title = 'our_community_blogs';
$table->classes = array( 'cBlogList', 'ipsAreaBackground', 'ipsDataList_large' );
/* Filters */
$table->filters = array(
'my_blogs' => array( '(' . \IPS\Db::i()->findInSet( 'blog_groupblog_ids', \IPS\Member::loggedIn()->groups ) . ' OR ' . 'blog_member_id=? )', \IPS\Member::loggedIn()->member_id ),
'blogs_with_content' => array( 'blog_count_entries>0' )
);
$featured = iterator_to_array( \IPS\blog\Entry::featured( 5, '_rand' ) );
$blogs = \IPS\blog\Blog::loadByOwner( \IPS\Member::loggedIn(), array( array( 'blog_disabled=?', 0 ) ) );
if ( ! \IPS\Settings::i()->blog_allow_grid )
{
$viewMode = 'list';
}
else
{
$viewMode = ( isset( \IPS\Request::i()->view ) ) ? \IPS\Request::i()->view : ( isset( \IPS\Request::i()->cookie['blog_view'] ) ? \IPS\Request::i()->cookie['blog_view'] : \IPS\Settings::i()->blog_view_mode );
}
if ( isset( \IPS\Request::i()->view ) )
{
\IPS\Request::i()->setCookie( 'blog_view', \IPS\Request::i()->view, ( new \IPS\DateTime )->add( new \DateInterval( 'P1Y' ) ) );
}
/* Grid view */
if ( $viewMode == 'grid' )
{
$perpage = 23;
$page = 1;
if ( \IPS\Request::i()->page )
{
$page = intval( \IPS\Request::i()->page );
if ( !$page OR $page < 1 )
{
$page = 1;
}
}
/* @note We cannot check individual member permissions here, so entries in draft status are excluded. */
$count = \IPS\blog\Entry::getItemsWithPermission( array( "entry_status!=?", 'draft' ), 'entry_date desc', NULL, 'read', \IPS\Content\Hideable::FILTER_AUTOMATIC, 0, NULL, FALSE, FALSE, FALSE, TRUE );
$entries = \IPS\blog\Entry::getItemsWithPermission( array( "entry_status!=?", 'draft' ), 'entry_date desc', array( ( $perpage * ( $page - 1 ) ), $perpage ) );
$pagination = array(
'page' => $page,
'pages' => ceil( $count / $perpage ),
'perpage' => $perpage,
'url' => \IPS\Http\Url::internal( 'app=blog&module=blogs&controller=browse', 'front', 'blogs' )
);
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'grid.css', 'blog', 'front' ) );
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'browse' )->indexGrid( $entries, $featured, $blogs, $pagination, $viewMode );
}
else
{
/* Blogs table */
$table = new \IPS\blog\Blog\Table( \IPS\Http\Url::internal( 'app=blog&module=blogs&controller=browse', 'front', 'blogs' ) );
$table->title = 'our_community_blogs';
$table->classes = array( 'cBlogList', 'ipsAreaBackground', 'ipsDataList_large' );
/* Filters */
$table->filters = array(
'my_blogs' => array( '(' . \IPS\Db::i()->findInSet( 'blog_groupblog_ids', \IPS\Member::loggedIn()->groups ) . ' OR ' . 'blog_member_id=? )', \IPS\Member::loggedIn()->member_id ),
'blogs_with_content' => array( 'blog_count_entries>0' )
);
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'browse' )->index( $table, $featured, $blogs, $viewMode );
}
\IPS\Session::i()->setLocation( \IPS\Http\Url::internal( 'app=blog', 'front', 'blogs' ), array(), 'loc_blog_viewing_index' );
/* Display */
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'front_browse.js', 'blog', 'front' ) );
\IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack('blogs');
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'browse' )->index( $table, $featured, $blogs );
}
}
@@ -110,7 +110,7 @@ class _entry extends \IPS\Content\Controller
'@context' => "http://schema.org",
'@type' => "Blog",
'url' => (string) $this->entry->container()->url(),
'name' => htmlspecialchars( $this->entry->container()->_title, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE ),
'name' => $this->entry->container()->_title,
'description' => strip_tags( $this->entry->container()->description ),
'commentCount' => $this->entry->container()->_comments,
'interactionStatistic' => array(
@@ -140,21 +140,21 @@ class _entry extends \IPS\Content\Controller
'@type' => "BlogPosting",
'url' => (string) $this->entry->url(),
'mainEntityOfPage' => (string) $this->entry->url(),
'name' => htmlspecialchars( $this->entry->mapped('title'), ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE ),
'headline' => htmlspecialchars( $this->entry->mapped('title'), ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE ),
'name' => $this->entry->mapped('title'),
'headline' => $this->entry->mapped('title'),
'articleBody' => strip_tags( $this->entry->truncated() ),
'commentCount' => $this->entry->mapped('num_comments'),
'dateCreated' => \IPS\DateTime::ts( $this->entry->date )->format( \IPS\DateTime::ISO8601 ),
'datePublished' => \IPS\DateTime::ts( $this->entry->publish_date )->format( \IPS\DateTime::ISO8601 ),
'author' => array(
'@type' => 'Person',
'name' => htmlspecialchars( \IPS\Member::load( $this->entry->mapped('author') )->name, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE ),
'name' => \IPS\Member::load( $this->entry->mapped('author') )->name,
'url' => (string) \IPS\Member::load( $this->entry->mapped('author') )->url(),
'image' => \IPS\Member::load( $this->entry->mapped('author') )->get_photo()
),
'publisher' => array(
'@type' => 'Organization',
'name' => htmlspecialchars( \IPS\Member::load( $this->entry->mapped('author') )->name, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE ),
'name' => \IPS\Member::load( $this->entry->mapped('author') )->name,
'url' => (string) \IPS\Member::load( $this->entry->mapped('author') )->url(),
'image' => \IPS\Member::load( $this->entry->mapped('author') )->get_photo(),
'logo' => array(
@@ -182,6 +182,15 @@ class _entry extends \IPS\Content\Controller
)
);
if( $this->entry->averageRating() )
{
\IPS\Output::i()->jsonLd['blog']['blogPost']['aggregateRating'] = array(
'@type' => 'AggregateRating',
'ratingValue' => $this->entry->averageRating(),
'ratingCount' => $this->entry->numberOfRatings(),
);
}
if( $this->entry->container()->coverPhoto()->file )
{
\IPS\Output::i()->jsonLd['blog']['image'] = (string) $this->entry->container()->coverPhoto()->file->url;
@@ -191,7 +200,7 @@ class _entry extends \IPS\Content\Controller
{
\IPS\Output::i()->jsonLd['blog']['author'] = array(
'@type' => 'Person',
'name' => htmlspecialchars( \IPS\Member::load( $this->entry->container()->member_id )->name, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE ),
'name' => \IPS\Member::load( $this->entry->container()->member_id )->name,
'url' => (string) \IPS\Member::load( $this->entry->container()->member_id )->url(),
'image' => \IPS\Member::load( $this->entry->container()->member_id )->get_photo()
);
@@ -235,6 +244,8 @@ class _entry extends \IPS\Content\Controller
}
/* File does not exist */
catch( \RuntimeException $e ) {}
/* Image is invalid */
catch( \InvalidArgumentException $e ){}
}
/* Display */
@@ -41,8 +41,8 @@ class _submit extends \IPS\Dispatcher\Controller
$this->blog = \IPS\blog\Blog::load( \IPS\Request::i()->id );
}
catch( \OutOfRangeException $e ) {}
\IPS\blog\Entry::canCreate( \IPS\Member::loggedIn(), $this->blog, TRUE );
}
\IPS\blog\Entry::canCreate( \IPS\Member::loggedIn(), $this->blog, TRUE );
parent::execute();
}
@@ -84,7 +84,7 @@ class _submit extends \IPS\Dispatcher\Controller
}
\IPS\Output::i()->breadcrumb[] = array( $this->blog->url(), $this->blog->_title );
\IPS\Session::i()->setLocation( $this->blog->url(), array(), 'loc_blog_adding_entry', $this->blog->member_id ? array( $this->blog->_title => FALSE ) : array( "blogs_blog_{$this->blog->id}" => TRUE ) );
\IPS\Session::i()->setLocation( $this->blog->url(), array(), 'loc_blog_adding_entry', array( $this->blog->_title => FALSE ) );
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'submit' )->submit( $formTemplate, $this->blog );
}
catch ( \OutOfRangeException $e )
@@ -95,7 +95,8 @@ class _submit extends \IPS\Dispatcher\Controller
'url' => \IPS\Http\Url::internal( 'app=blog&module=blogs&controller=submit', 'front', 'blog_submit' ),
'class' => 'IPS\blog\Blog',
'permissionCheck' => 'add',
'forceOwner' => \IPS\Member::loggedIn(),
'forceOwner' => \IPS\Member::loggedIn(),
'clubs' => \IPS\Settings::i()->club_nodes_in_apps
) ) );
if ( $values = $form->values() )
+16 -6
View File
@@ -63,14 +63,24 @@ class _view extends \IPS\Helpers\CoverPhoto\Controller
{
/* Build table */
$where = array();
if ( !in_array( $this->blog->id, array_keys( \IPS\blog\Blog::loadByOwner( \IPS\Member::loggedIn() ) ) ) )
if ( !in_array( $this->blog->id, array_keys( \IPS\blog\Blog::loadByOwner( \IPS\Member::loggedIn() ) ) ) AND !\IPS\blog\Entry::canViewHiddenItems( \IPS\Member::loggedIn(), $this->blog ) )
{
$where[] = array( "entry_status!='draft'" );
}
$table = new \IPS\Helpers\Table\Content( 'IPS\blog\Entry', $this->blog->url(), $where, $this->blog );
$table->tableTemplate = array( \IPS\Theme::i()->getTemplate( 'view' ), 'blogTable' );
$table->rowsTemplate = array( \IPS\Theme::i()->getTemplate( 'view' ), 'rows' );
if ( \IPS\Settings::i()->blog_allow_grid )
{
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'grid.css', 'blog', 'front' ) );
$table->rowsTemplate = array( \IPS\Theme::i()->getTemplate( 'view' ), 'rowsGrid' );
}
else
{
$table->rowsTemplate = array( \IPS\Theme::i()->getTemplate( 'view' ), 'rows' );
}
$table->title = \IPS\Member::loggedIn()->language()->addToStack('entries_in_this_blog');
$table->sortBy = \IPS\Request::i()->sortby ?: 'date';
@@ -85,7 +95,7 @@ class _view extends \IPS\Helpers\CoverPhoto\Controller
);
/* Online User Location */
\IPS\Session::i()->setLocation( $this->blog->url(), array(), 'loc_blog_viewing_blog', $this->blog->member_id ? array( $this->blog->_title => FALSE ) : array( "blogs_blog_{$this->blog->id}" => TRUE ) );
\IPS\Session::i()->setLocation( $this->blog->url(), array(), 'loc_blog_viewing_blog', array( "blogs_blog_{$this->blog->id}" => TRUE ) );
/* Output */
\IPS\Output::i()->title = $this->blog->_title;
@@ -100,7 +110,7 @@ class _view extends \IPS\Helpers\CoverPhoto\Controller
'@context' => "http://schema.org",
'@type' => "Blog",
'url' => (string) $this->blog->url(),
'name' => htmlspecialchars( $this->blog->_title, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE ),
'name' => $this->blog->_title,
'description' => strip_tags( $this->blog->description ),
'commentCount' => $this->blog->_comments,
'interactionStatistic' => array(
@@ -136,7 +146,7 @@ class _view extends \IPS\Helpers\CoverPhoto\Controller
{
\IPS\Output::i()->jsonLd['blog']['author'] = array(
'@type' => 'Person',
'name' => htmlspecialchars( \IPS\Member::load( $this->blog->member_id )->name, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE ),
'name' => \IPS\Member::load( $this->blog->member_id )->name,
'url' => (string) \IPS\Member::load( $this->blog->member_id )->url(),
'image' => \IPS\Member::load( $this->blog->member_id )->get_photo()
);
@@ -313,7 +323,7 @@ class _view extends \IPS\Helpers\CoverPhoto\Controller
$form->add( new \IPS\Helpers\Form\Text( 'blog_rss_import_auth_pass', $feed ? $feed->auth_pass : NULL, FALSE, array(), NULL, NULL, NULL, 'blog_rss_import_auth_pass' ) );
$form->add( new \IPS\Helpers\Form\Text( 'blog_rss_import_show_link', $feed->import_show_link ?: \IPS\Member::loggedIn()->language()->addToStack('blog_rss_import_show_link_default' ), FALSE, array(), NULL, NULL, NULL, 'blog_rss_import_show_link' ) );
$options = array( 'autocomplete' => array( 'unique' => TRUE, 'source' => \IPS\Content\Item::definedTags( $this->blog ), 'freeChoice' => ( \IPS\Settings::i()->tags_open_system ? TRUE : FALSE ) ) );
$options = array( 'autocomplete' => array( 'unique' => TRUE, 'minimized' => FALSE, 'source' => \IPS\Content\Item::definedTags( $this->blog ), 'freeChoice' => ( \IPS\Settings::i()->tags_open_system ? TRUE : FALSE ) ) );
if ( \IPS\Settings::i()->tags_force_lower )
{
$options['autocomplete']['forceLower'] = TRUE;
@@ -0,0 +1,37 @@
{
"1": {
"method": "addIndex",
"params": [
"blog_entries",
{
"type": "key",
"name": "entry_author_id",
"columns": [
"entry_author_id"
],
"length": [
null
]
}
]
},
"2": {
"method": "changeIndex",
"params": [
"blog_entries",
"entry_author_id",
{
"type": "key",
"name": "entry_author_id",
"columns": [
"entry_author_id",
"entry_last_update"
],
"length": [
null,
null
]
}
]
}
}
@@ -0,0 +1,43 @@
<?php
/**
* @brief 4.3.0 Beta 1 Upgrade Code
* @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 Blogs
* @since 04 Jan 2018
*/
namespace IPS\blog\setup\upg_103000;
/* 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;
}
/**
* 4.3.0 Beta 1 Upgrade Code
*/
class _Upgrade
{
/**
* ...
*
* @return array If returns TRUE, upgrader will proceed to next step. If it returns any other value, it will set this as the value of the 'extra' GET parameter and rerun this step (useful for loops)
*/
public function step1()
{
/* Move blog names into language system */
foreach( \IPS\Db::i()->select( '*', 'blog_blogs', array( 'blog_name IS NOT NULL' ) ) as $blog )
{
if ( ! empty( $blog['blog_name'] ) )
{
\IPS\Lang::saveCustom( 'blog', 'blogs_blog_' . $blog['blog_id'], $blog['blog_name'] );
}
}
return TRUE;
}
}
+89 -62
View File
@@ -102,6 +102,11 @@ class _Blog extends \IPS\Node\Model implements \IPS\Node\Ratings, \IPS\Content\E
* @brief Cover Photo Storage Extension
*/
public static $coverPhotoStorageExtension = 'blog_Blogs';
/**
* @brief Use a default cover photo
*/
public static $coverPhotoDefault = true;
/**
* Columns needed to query for search result / stream view
@@ -111,7 +116,6 @@ class _Blog extends \IPS\Node\Model implements \IPS\Node\Ratings, \IPS\Content\E
public static function basicDataColumns()
{
$return = parent::basicDataColumns();
$return[] = 'blog_name';
$return[] = 'blog_member_id';
return $return;
}
@@ -123,7 +127,7 @@ class _Blog extends \IPS\Node\Model implements \IPS\Node\Ratings, \IPS\Content\E
*/
public static function contentTableTemplate()
{
return array( \IPS\Theme::i()->getTemplate( 'browse', 'blog' ), 'rows' );
return array( \IPS\Theme::i()->getTemplate( 'browse', 'blog', 'front' ), 'rows' );
}
/**
@@ -149,19 +153,6 @@ class _Blog extends \IPS\Node\Model implements \IPS\Node\Ratings, \IPS\Content\E
return FALSE;
}
/**
* Get title from index data
*
* @param array $indexData Data from the search index
* @param array $itemData Basic data about the item. Only includes columns returned by item::basicDataColumns()
* @param array|NULL $containerData Basic data about the author. Only includes columns returned by container::basicDataColumns()
* @return \IPS\Http\Url
*/
public static function titleFromIndexData( $indexData, $itemData, $containerData )
{
return ( $containerData['blog_member_id'] ) ? $containerData['blog_name'] : parent::titleFromIndexData( $indexData, $itemData, $containerData );
}
/**
* Check permissions
*
@@ -258,7 +249,7 @@ class _Blog extends \IPS\Node\Model implements \IPS\Node\Ratings, \IPS\Content\E
if ( $club = $this->club() )
{
if ( $club->type == $club::TYPE_PUBLIC )
if ( $club->type == $club::TYPE_PUBLIC or $club->type == $club::TYPE_READONLY )
{
return 'ca';
}
@@ -286,21 +277,33 @@ class _Blog extends \IPS\Node\Model implements \IPS\Node\Ratings, \IPS\Content\E
/**
* Additional WHERE clauses for Follow view
*
* @param array $joins Joins
* @return array
*/
public static function followWhere()
public static function followWhere( &$joins )
{
$where = array();
if ( \IPS\Member::loggedIn()->member_id )
{
$where[] = array( '( blog_blogs.blog_social_group IS NULL OR blog_blogs.blog_member_id=' . \IPS\Member::loggedIn()->member_id . ' OR ( ' . \IPS\Content::socialGroupGetItemsWithPermissionWhere( 'blog_blogs.blog_social_group', \IPS\Member::loggedIn() ) . ' ) )' );
$where[] = array( '( blog_blogs.blog_club_id IS NULL OR ' . \IPS\Db::i()->in( 'blog_blogs.blog_club_id', \IPS\Member::loggedIn()->clubs() ) . ' )' );
}
else
{
$where[] = \IPS\Content::socialGroupGetItemsWithPermissionWhere( 'blog_blogs.blog_social_group', \IPS\Member::loggedIn() );
$where[] = array( 'blog_blogs.blog_club_id IS NULL' );
}
if ( \IPS\Settings::i()->clubs )
{
$joins[] = array( 'from' => 'core_clubs', 'where' => 'core_clubs.id=blog_blogs.blog_club_id' );
if ( \IPS\Member::loggedIn()->member_id )
{
$where[] = array( '( blog_blogs.blog_club_id IS NULL OR ' . \IPS\Db::i()->in( 'blog_blogs.blog_club_id', \IPS\Member::loggedIn()->clubs() ) . ' OR core_clubs.type=? OR core_clubs.type=? )', \IPS\Member\Club::TYPE_PUBLIC, \IPS\Member\Club::TYPE_READONLY );
}
else
{
$where[] = array( '( blog_blogs.blog_club_id IS NULL OR core_clubs.type=? OR core_clubs.type=? )', \IPS\Member\Club::TYPE_PUBLIC, \IPS\Member\Club::TYPE_READONLY );
}
}
return $where;
@@ -409,7 +412,7 @@ class _Blog extends \IPS\Node\Model implements \IPS\Node\Ratings, \IPS\Content\E
throw new \InvalidArgumentException( 'form_required' );
}
}, NULL, NULL, 'blog_name' ) );
$form->add( new \IPS\Helpers\Form\Editor( 'blog_desc', $this->id ? $this->desc : NULL, FALSE, array( 'app' => 'blog', 'key' => 'Blogs', 'autoSaveKey' => ( $this->id ? "blogs-blog-{$this->id}m" : "blogs-new-blog" ), 'attachIds' => $this->id ? array( $this->id, NULL, 'description' ) : NULL, 'minimize' => 'blog_desc_placeholder' ), NULL, NULL, NULL, 'blog_desc_wrap' ) );
$form->add( new \IPS\Helpers\Form\Editor( 'blog_desc', $this->id ? $this->description : NULL, FALSE, array( 'app' => 'blog', 'key' => 'Blogs', 'autoSaveKey' => ( $this->id ? "blogs-blog-{$this->id}m" : "blogs-new-blog" ), 'attachIds' => $this->id ? array( $this->id, NULL, 'description' ) : NULL, 'minimize' => 'blog_desc_placeholder' ), NULL, NULL, NULL, 'blog_desc_wrap' ) );
/* Group blogs - only one or the other will show at any given time */
$form->add( new \IPS\Helpers\Form\Translatable( 'blog_name_group', ( $this->id AND !$this->groupblog_ids ) ? $this->_title : NULL, FALSE, array( 'app' => 'blog', 'key' => ( $this->id ? "blogs_blog_{$this->id}" : NULL ) ), function( $value ) {
@@ -552,12 +555,7 @@ class _Blog extends \IPS\Node\Model implements \IPS\Node\Ratings, \IPS\Content\E
unset( $values[ $fieldKey ] );
}
}
if( array_key_exists( 'blog_name', $values ) )
{
unset( $values['blog_name'] );
}
if( array_key_exists( 'blog_desc', $values ) )
{
unset( $values['blog_desc'] );
@@ -584,13 +582,18 @@ class _Blog extends \IPS\Node\Model implements \IPS\Node\Ratings, \IPS\Content\E
}
}
\IPS\Lang::saveCustom( 'blog', "blogs_blog_{$this->id}", $values['blog_name'] );
/* This is here in case an admin changes a group blog to a member blog */
\IPS\Lang::deleteCustom( 'blog', "blogs_blog_{$this->id}" );
\IPS\Lang::deleteCustom( 'blog', "blogs_blog_{$this->id}_desc" );
\IPS\Lang::deleteCustom( 'blog', "blogs_groupblog_name_{$this->id}" );
$values['seo_name'] = \IPS\Http\Url\Friendly::seoTitle( $values['blog_name'] );
}
if( array_key_exists( 'blog_name', $values ) )
{
unset( $values['blog_name'] );
}
if( array_key_exists( 'blog_name_group', $values ) )
{
@@ -758,7 +761,7 @@ class _Blog extends \IPS\Node\Model implements \IPS\Node\Ratings, \IPS\Content\E
try
{
/* @note entry_hidden is flipped to map to "approved" */
/* @note entry_hidden is flipped to map to "approved" and that this method will always only return the latest, visible, entry. */
$this->latestEntry = \IPS\blog\Entry::constructFromData( \IPS\Db::i()->select( '*', 'blog_entries', array( 'entry_blog_id=? AND entry_is_future_entry=0 AND entry_hidden=1 AND entry_status!=?', $this->_id, 'draft' ), 'entry_date DESC', 1 )->first() );
return $this->latestEntry;
}
@@ -783,7 +786,7 @@ class _Blog extends \IPS\Node\Model implements \IPS\Node\Ratings, \IPS\Content\E
$select = \IPS\Db::i()->select(
"entry_author_id, count( entry_id ) as contributions",
'blog_entries',
array( "entry_blog_id=? AND entry_author_id !=?", $this->id, 0 ),
array( "entry_blog_id=? AND entry_author_id !=? AND entry_hidden!=?", $this->id, 0, -2 ),
"contributions DESC",
NULL,
array( 'entry_author_id' )
@@ -932,33 +935,6 @@ class _Blog extends \IPS\Node\Model implements \IPS\Node\Ratings, \IPS\Content\E
$this->count_entries_future = $val;
}
/**
* Returns the title
*
* @return string
*/
protected function get__title()
{
return $this->member_id ? $this->name : \IPS\Member::loggedIn()->language()->addToStack( static::$titleLangPrefix . $this->_id );
}
/**
* Node titles can contain HTML. Apparently.
*
* @return string
*/
public function get__stripTagsTitle()
{
if ( $this->member_id )
{
return $this->name;
}
else
{
return \IPS\Member::loggedIn()->language()->addToStack( static::$titleLangPrefix . $this->_id, NULL, array( 'striptags' => true ) );
}
}
/**
* Returns the title
*
@@ -968,7 +944,7 @@ class _Blog extends \IPS\Node\Model implements \IPS\Node\Ratings, \IPS\Content\E
{
return $this->member_id ? $this->desc : \IPS\Member::loggedIn()->language()->addToStack( static::$titleLangPrefix . $this->_id . static::$descriptionLangSuffix );
}
/**
* Get settings
*
@@ -1074,6 +1050,16 @@ class _Blog extends \IPS\Node\Model implements \IPS\Node\Ratings, \IPS\Content\E
$photo->overlay = \IPS\Theme::i()->getTemplate( 'view', 'blog' )->coverPhotoOverlay( $this );
return $photo;
}
/**
* Produce a random hex color for a background
*
* @return string
*/
public function coverPhotoBackgroundColor()
{
return $this->staticCoverPhotoBackgroundColor( $this->titleForLog() );
}
/**
* Get content for embed
@@ -1105,6 +1091,7 @@ class _Blog extends \IPS\Node\Model implements \IPS\Node\Ratings, \IPS\Content\E
/**
* Get output for API
*
* @param \IPS\Member|NULL $authorizedMember The member making the API request or NULL for API Key / client_credentials
* @return array
* @apiresponse int id ID number
* @apiresponse string name Name
@@ -1116,7 +1103,7 @@ class _Blog extends \IPS\Node\Model implements \IPS\Node\Ratings, \IPS\Content\E
* @apiresponse int comments Number of comments
* @apiresponse string url URL
*/
public function apiOutput()
public function apiOutput( \IPS\Member $authorizedMember = NULL )
{
$groups = array();
if ( $this->groupblog_ids )
@@ -1125,7 +1112,7 @@ class _Blog extends \IPS\Node\Model implements \IPS\Node\Ratings, \IPS\Content\E
{
try
{
$groups[] = \IPS\Member\Group::load( $groupId )->apiOutput();
$groups[] = \IPS\Member\Group::load( $groupId )->apiOutput( $authorizedMember );
}
catch ( \OutOfRangeException $e ) { }
}
@@ -1134,8 +1121,8 @@ class _Blog extends \IPS\Node\Model implements \IPS\Node\Ratings, \IPS\Content\E
return array(
'id' => $this->id,
'name' => $this->_title,
'description' => $this->member_id ? $this->desc : \IPS\Member::loggedIn()->language()->addToStack("blogs_blog_{$this->id}_desc"),
'owner' => $this->member_id ? $this->owner()->apiOutput() : null,
'description' => \IPS\Member::loggedIn()->language()->addToStack("blogs_blog_{$this->id}_desc"),
'owner' => $this->member_id ? $this->owner()->apiOutput( $authorizedMember ) : null,
'groups' => $groups,
'pinned' => (bool) $this->pinned,
'entries' => $this->count_entries,
@@ -1203,6 +1190,12 @@ class _Blog extends \IPS\Node\Model implements \IPS\Node\Ratings, \IPS\Content\E
{
$this->seo_name = \IPS\Http\Url\Friendly::seoTitle( $values['club_node_name'] );
}
if ( !$this->_id )
{
$this->save();
\IPS\File::claimAttachments( 'blogs-new-blog', $this->id, NULL, 'description' );
}
}
/**
@@ -1215,4 +1208,38 @@ class _Blog extends \IPS\Node\Model implements \IPS\Node\Ratings, \IPS\Content\E
{
// Deliberately do nothing, Blog handles permissions differently
}
/**
* Fetch All Nodes in Clubs
*
* @param string|NULL $permissionCheck The permission key to check for or NULl to not check permissions
* @param \IPS\Member|NULL $member The member to check permissions for or NULL for the currently logged in member
* @param mixed $where Additional WHERE clause
* @return array
*/
public static function clubNodes( $permissionCheck='view', $member=NULL, $where=array() )
{
$member = $member ?: \IPS\Member::loggedIn();
if ( $member->modPermission('can_access_all_clubs') )
{
return static::nodesWithPermission( NULL, $member, $where );
}
else
{
$where[] = array( 'blog_club_id IS NOT NULL' );
if ( $permissionCheck === 'add' )
{
$statuses = array( \IPS\Member\Club::STATUS_LEADER, \IPS\Member\Club::STATUS_MODERATOR );
}
else
{
$statuses = array( \IPS\Member\Club::STATUS_LEADER, \IPS\Member\Club::STATUS_MODERATOR, \IPS\Member\Club::STATUS_MEMBER );
}
$where[] = array( \IPS\Db::i()->in( 'status', $statuses ) );
return new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'blog_blogs', $where )->join( 'core_clubs_memberships', array( 'club_id=blog_club_id AND member_id=?', \IPS\Member::loggedIn()->member_id ) ), get_called_class() );
}
}
}
+13 -3
View File
@@ -67,7 +67,7 @@ class _Feed extends \IPS\Patterns\ActiveRecord
$i = 0;
$request = $request->decodeXml();
if( !( $request instanceof \IPS\Xml\RssOne ) AND !( $request instanceof \IPS\Xml\Rss ) AND !( $request instanceof \IPS\Xml\Atom ) )
if( !( $request instanceof \IPS\Xml\Rss1 ) AND !( $request instanceof \IPS\Xml\Rss ) AND !( $request instanceof \IPS\Xml\Atom ) )
{
throw new \RuntimeException( 'rss_import_invalid' );
}
@@ -76,6 +76,12 @@ class _Feed extends \IPS\Patterns\ActiveRecord
{
if ( !in_array( $guid, $previouslyImportedGuids ) )
{
/* Don't post entries in the future */
if( $article['date']->getTimestamp() > time() )
{
$article['date'] = \IPS\DateTime::create();
}
$entry = \IPS\blog\Entry::createItem( \IPS\Member::load( $this->member ), NULL, $article['date'], $container );
$entry->name = $article['title'];
$entry->content = \IPS\Text\Parser::parseStatic( $article['content'], TRUE );
@@ -100,7 +106,7 @@ class _Feed extends \IPS\Patterns\ActiveRecord
$linkRelPart = 'rel="' . implode($rel, ' ') . '"';
}
$link = htmlspecialchars( $this->import_show_link, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE );
$link = htmlspecialchars( $this->import_show_link, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', FALSE );
$entry->content .= "<br><p><a href='{$article['link']}' {$linkRelPart}>{$link}</a></p>";
}
@@ -109,7 +115,11 @@ class _Feed extends \IPS\Patterns\ActiveRecord
/* Add to search index */
\IPS\Content\Search\Index::i()->index( $entry );
/* Send notifications */
$entry->sendNotifications();
$entry->setTags( json_decode( $this->tags, TRUE ) );
$inserts[] = array(
+23 -2
View File
@@ -99,8 +99,29 @@ class _Table extends \IPS\Helpers\Table\Table
$where[] = array( 'blog_social_group IS NULL' );
}
/* Exclude club blogs */
$where[] = array( 'blog_club_id IS NULL' );
/* Exclude club blogs unless we have permission to view them */
if ( \IPS\Settings::i()->club_nodes_in_apps )
{
if ( \IPS\Member::loggedIn()->member_id )
{
$where[] = array(
'( blog_club_id IS NULL OR blog_club_id IN(?) OR blog_club_id IN(?) )',
\IPS\Db::i()->select( 'id', 'core_clubs', array( \IPS\Db::i()->in( 'type', array( \IPS\Member\Club::TYPE_PUBLIC, \IPS\Member\Club::TYPE_OPEN, \IPS\Member\Club::TYPE_READONLY ) ) ) ),
\IPS\Db::i()->select( 'club_id', 'core_clubs_memberships', array( "member_id=? AND status IN('" . \IPS\Member\Club::STATUS_MEMBER . "','" . \IPS\Member\Club::STATUS_MODERATOR . "','" . \IPS\Member\Club::STATUS_LEADER . "')", \IPS\Member::loggedIn()->member_id ) ),
);
}
else
{
$where[] = array(
'( blog_club_id IS NULL OR blog_club_id IN(?) )',
\IPS\Db::i()->select( 'id', 'core_clubs', array( \IPS\Db::i()->in( 'type', array( \IPS\Member\Club::TYPE_PUBLIC, \IPS\Member\Club::TYPE_OPEN, \IPS\Member\Club::TYPE_READONLY ) ) ) ),
);
}
}
else
{
$where[] = array( 'blog_club_id IS NULL' );
}
/* Get Count */
$count = \IPS\Db::i()->select( 'COUNT(*) as cnt', 'blog_blogs', $where )->first();
+16 -4
View File
@@ -21,9 +21,9 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
/**
* Blog Entry Comment Model
*/
class _Comment extends \IPS\Content\Comment implements \IPS\Content\EditHistory, \IPS\Content\ReportCenter, \IPS\Content\Hideable, \IPS\Content\Searchable, \IPS\Content\Embeddable
class _Comment extends \IPS\Content\Comment implements \IPS\Content\EditHistory, \IPS\Content\Hideable, \IPS\Content\Searchable, \IPS\Content\Embeddable
{
use \IPS\Content\Reactable;
use \IPS\Content\Reactable, \IPS\Content\Reportable;
/**
* @brief [ActiveRecord] Multiton Store
@@ -105,13 +105,25 @@ class _Comment extends \IPS\Content\Comment implements \IPS\Content\EditHistory,
if ( $member->member_id )
{
$where[] = array( '( blog_blogs.blog_social_group IS NULL OR blog_blogs.blog_member_id=' . $member->member_id . ' OR ( ' . \IPS\Content::socialGroupGetItemsWithPermissionWhere( 'blog_blogs.blog_social_group', $member ) . ' ) )' );
$where[] = array( '( blog_blogs.blog_club_id IS NULL OR ' . \IPS\Db::i()->in( 'blog_blogs.blog_club_id', \IPS\Member::loggedIn()->clubs() ) . ' )' );
}
else
{
$where[] = array( "(" . \IPS\Content::socialGroupGetItemsWithPermissionWhere( 'blog_blogs.blog_social_group', $member ) . " OR blog_blogs.blog_social_group IS NULL )" );
$where[] = array( 'blog_blogs.blog_club_id IS NULL' );
}
if ( \IPS\Settings::i()->clubs )
{
$joins[] = array( 'from' => 'core_clubs', 'where' => 'core_clubs.id=blog_blogs.blog_club_id' );
if ( $member->member_id )
{
$where[] = array( '( blog_blogs.blog_club_id IS NULL OR ' . \IPS\Db::i()->in( 'blog_blogs.blog_club_id', $member->clubs() ) . ' OR core_clubs.type=? OR core_clubs.type=? )', \IPS\Member\Club::TYPE_PUBLIC, \IPS\Member\Club::TYPE_READONLY );
}
else
{
$where[] = array( '( blog_blogs.blog_club_id IS NULL OR core_clubs.type=? OR core_clubs.type=? )', \IPS\Member\Club::TYPE_PUBLIC, \IPS\Member\Club::TYPE_READONLY );
}
}
}
return parent::getItemsWithPermission( $where, $order, $limit, $permissionKey, $includeHiddenItems, $queryFlags, $member, $joinContainer, $joinComments, $joinReviews, $countOnly, $joins );
}
+79 -12
View File
@@ -26,7 +26,6 @@ class _Entry extends \IPS\Content\Item implements
\IPS\Content\Tags,
\IPS\Content\Followable,
\IPS\Content\Shareable,
\IPS\Content\ReportCenter,
\IPS\Content\ReadMarkers,
\IPS\Content\Views,
\IPS\Content\Polls,
@@ -37,7 +36,7 @@ class _Entry extends \IPS\Content\Item implements
\IPS\Content\FuturePublishing,
\IPS\Content\MetaData
{
use \IPS\Content\Reactable;
use \IPS\Content\Reactable, \IPS\Content\Reportable;
/**
* @brief Multiton Store
@@ -140,6 +139,16 @@ class _Entry extends \IPS\Content\Item implements
*/
public static $hideLogKey = 'blog-entry';
/**
* @brief [CoverPhoto] Storage extension
*/
public static $coverPhotoStorageExtension = 'blog_Entries';
/**
* @brief Use a default cover photo
*/
public static $coverPhotoDefault = true;
/**
* Set the title
*
@@ -298,17 +307,29 @@ class _Entry extends \IPS\Content\Item implements
if ( in_array( $permissionKey, array( 'view', 'read' ) ) )
{
$joinContainer = TRUE;
$member = $member ?: \IPS\Member::loggedIn();
if ( $member->member_id )
{
$where[] = array( '( blog_blogs.blog_member_id=' . $member->member_id . ' OR ( ' . \IPS\Content::socialGroupGetItemsWithPermissionWhere( 'blog_blogs.blog_social_group', $member ) . ' ) OR blog_blogs.blog_social_group IS NULL )' );
$where[] = array( '( blog_blogs.blog_club_id IS NULL OR ' . \IPS\Db::i()->in( 'blog_blogs.blog_club_id', \IPS\Member::loggedIn()->clubs() ) . ' )' );
}
else
{
$where[] = array( "(" . \IPS\Content::socialGroupGetItemsWithPermissionWhere( 'blog_blogs.blog_social_group', $member ) . " OR blog_blogs.blog_social_group IS NULL )" );
$where[] = array( 'blog_blogs.blog_club_id IS NULL' );
}
if ( \IPS\Settings::i()->clubs )
{
$joins[] = array( 'from' => 'core_clubs', 'where' => 'core_clubs.id=blog_blogs.blog_club_id' );
if ( $member->member_id )
{
$where[] = array( '( blog_blogs.blog_club_id IS NULL OR ' . \IPS\Db::i()->in( 'blog_blogs.blog_club_id', $member->clubs() ) . ' OR core_clubs.type=? OR core_clubs.type=? )', \IPS\Member\Club::TYPE_PUBLIC, \IPS\Member\Club::TYPE_READONLY );
}
else
{
$where[] = array( '( blog_blogs.blog_club_id IS NULL OR core_clubs.type=? OR core_clubs.type=? )', \IPS\Member\Club::TYPE_PUBLIC, \IPS\Member\Club::TYPE_READONLY );
}
}
}
return parent::getItemsWithPermission( $where, $order, $limit, $permissionKey, $includeHiddenItems, $queryFlags, $member, $joinContainer, $joinComments, $joinReviews, $countOnly, $joins, $skipPermission, $joinTags, $joinAuthor, $joinLastCommenter, $showMovedLinks );
}
@@ -326,12 +347,23 @@ class _Entry extends \IPS\Content\Item implements
if ( \IPS\Member::loggedIn()->member_id )
{
$where = array( array( '( blog_blogs.blog_social_group IS NULL OR blog_blogs.blog_member_id=' . \IPS\Member::loggedIn()->member_id . ' OR ( ' . \IPS\Content::socialGroupGetItemsWithPermissionWhere( 'blog_blogs.blog_social_group', \IPS\Member::loggedIn() ) . ' ) )' ) );
$where[] = array( '( blog_blogs.blog_club_id IS NULL OR ' . \IPS\Db::i()->in( 'blog_blogs.blog_club_id', \IPS\Member::loggedIn()->clubs() ) . ' )' );
}
else
{
$where = array( \IPS\Content::socialGroupGetItemsWithPermissionWhere( 'blog_blogs.blog_social_group', \IPS\Member::loggedIn() ) );
$where[] = array( 'blog_blogs.blog_club_id IS NULL' );
}
if ( \IPS\Settings::i()->clubs )
{
$joins[] = array( 'from' => 'core_clubs', 'where' => 'core_clubs.id=blog_blogs.blog_club_id' );
if ( \IPS\Member::loggedIn()->member_id )
{
$where[] = array( '( blog_blogs.blog_club_id IS NULL OR ' . \IPS\Db::i()->in( 'blog_blogs.blog_club_id', \IPS\Member::loggedIn()->clubs() ) . ' OR core_clubs.type=? OR core_clubs.type=? )', \IPS\Member\Club::TYPE_PUBLIC, \IPS\Member\Club::TYPE_READONLY );
}
else
{
$where[] = array( '( blog_blogs.blog_club_id IS NULL OR core_clubs.type=? OR core_clubs.type=? )', \IPS\Member\Club::TYPE_PUBLIC, \IPS\Member\Club::TYPE_READONLY );
}
}
return array_merge( parent::followWhere( $joinContainer, $joins ), $where );
@@ -358,6 +390,8 @@ class _Entry extends \IPS\Content\Item implements
'permissionCheck' => 'add',
) );
}
$return['image'] = new \IPS\Helpers\Form\Upload( 'blog_entry_cover_photo', ( ( $item AND $item->cover_photo ) ? \IPS\File::get( 'blog_Entries', $item->cover_photo ) : NULL ), FALSE, array( 'storageExtension' => 'blog_Entries', 'image' => array( 'maxWidth' => 4800, 'maxHeight' => 4800 ) ) );
$return['publish'] = new \IPS\Helpers\Form\YesNo( 'blog_entry_publish', $item ? $item->status : TRUE, FALSE, array( 'togglesOn' => array( 'blog_entry_date' ) ) );
@@ -411,6 +445,8 @@ class _Entry extends \IPS\Content\Item implements
{
$this->date = ( $values['blog_entry_date'] AND $values['blog_entry_publish'] ) ? $values['blog_entry_date']->getTimestamp() : time();
}
$this->cover_photo = (string) $values['blog_entry_cover_photo'];
/* Gallery album association */
if( \IPS\Application::appIsEnabled( 'gallery' ) AND $values['entry_gallery_album'] instanceof \IPS\gallery\Album )
@@ -600,7 +636,7 @@ class _Entry extends \IPS\Content\Item implements
*/
public static function contentTableTemplate()
{
return array( \IPS\Theme::i()->getTemplate( 'global', 'blog' ), 'rows' );
return array( \IPS\Theme::i()->getTemplate( 'global', 'blog', 'front' ), 'rows' );
}
/**
@@ -634,6 +670,7 @@ class _Entry extends \IPS\Content\Item implements
/**
* Get output for API
*
* @param \IPS\Member|NULL $authorizedMember The member making the API request or NULL for API Key / client_credentials
* @return array
* @apiresponse int id ID number
* @apiresponse string title Title
@@ -655,13 +692,13 @@ class _Entry extends \IPS\Content\Item implements
* @apiresponse string url URL
* @apiresponse float rating Average Rating
*/
public function apiOutput()
public function apiOutput( \IPS\Member $authorizedMember = NULL )
{
return array(
'id' => $this->id,
'title' => $this->name,
'blog' => $this->container()->apiOutput(),
'author' => $this->author()->apiOutput(),
'blog' => $this->container()->apiOutput( $authorizedMember ),
'author' => $this->author()->apiOutput( $authorizedMember ),
'draft' => $this->status == 'draft',
'date' => \IPS\DateTime::ts( $this->date )->rfc3339(),
'entry' => $this->content(),
@@ -674,7 +711,7 @@ class _Entry extends \IPS\Content\Item implements
'future' => $this->isFutureDate(),
'pinned' => (bool) $this->mapped('pinned'),
'featured' => (bool) $this->mapped('featured'),
'poll' => $this->poll_state ? \IPS\Poll::load( $this->poll_state )->apiOutput() : null,
'poll' => $this->poll_state ? \IPS\Poll::load( $this->poll_state )->apiOutput( $authorizedMember ) : null,
'url' => (string) $this->url(),
'rating' => $this->averageRating(),
);
@@ -706,7 +743,6 @@ class _Entry extends \IPS\Content\Item implements
* @param string The action
* @param \IPS\Member|NULL The member, or NULL for currently logged in
* @return bool
* @todo Mod Log
*/
public function canOnMessage( $action, \IPS\Member $member = NULL )
{
@@ -770,6 +806,37 @@ class _Entry extends \IPS\Content\Item implements
return $return;
}
/**
* Returns the content images
*
* @param int|null $limit Number of attachments to fetch, or NULL for all
*
* @return array|NULL
* @throws \BadMethodCallException
*/
public function contentImages( $limit = NULL )
{
$idColumn = static::$databaseColumnId;
$internal = NULL;
$attachments = array();
if ( isset( static::$databaseColumnMap['content'] ) )
{
$internal = \IPS\Db::i()->select( 'attachment_id', 'core_attachments_map', array( 'location_key=? and id1=?', 'blog_Entries', $this->$idColumn ) );
}
if ( $internal )
{
foreach( \IPS\Db::i()->select( '*', 'core_attachments', array( array( 'attach_id IN(?)', $internal ), array( 'attach_is_image=1' ) ), 'attach_id ASC', $limit ) as $row )
{
$attachments[] = array( 'core_Attachment' => $row['attach_location'] );
}
}
return count( $attachments ) ? $attachments : NULL;
}
/**
* Get content for embed
+1 -1
View File
@@ -107,7 +107,7 @@ class _blogs extends \IPS\Widget\PermissionCache
'blog_last_edate' => 'widget__blog_edate',
'blog_count_entries' => 'widget__blog_count_entries',
'blog_count_comments' => 'widget__blog_count_comments'
) ) ), NULL, NULL, NULL, 'widget_feed_sort_on' );
) ), NULL, NULL, NULL, 'widget_feed_sort_on' ) );
$form->add( new \IPS\Helpers\Form\Select( 'widget_feed_sort_dir', isset( $this->configuration['widget_feed_sort_dir'] ) ? $this->configuration['widget_feed_sort_dir'] : 'desc', FALSE, array(
'options' => array(
+1 -1
View File
@@ -145,7 +145,7 @@ class _Application extends \IPS\Application
foreach ( \IPS\calendar\Event::retrieveEvents( $_today, $endDate, NULL, NULL, FALSE, $member ) as $event )
{
$document->addItem( $event->title, $event->url(), $event->content, $event->nextOccurrence( $_today, 'startDate' ), $event->id );
$document->addItem( $event->title, $event->url(), $event->content, \IPS\DateTime::ts( $event->saved ), $event->id );
}
/* @note application/rss+xml is not a registered IANA mime-type so we need to stick with text/xml for RSS */
+24 -15
View File
@@ -29,9 +29,12 @@ class _calendars extends \IPS\Node\Api\NodeController
protected $class = 'IPS\calendar\Calendar';
/**
* GET /calendar/calendar
* GET /calendar/calendars
* Get list of calendars
*
* @apiparam int page Page number
* @apiparam int perPage Number of results per page - defaults to 25
* @note For requests using an OAuth Access Token for a particular member, only calendars the authorized user can view will be included
* @return \IPS\Api\PaginatedResponse<IPS\calendar\Calendar>
*/
public function GETindex()
@@ -41,23 +44,30 @@ class _calendars extends \IPS\Node\Api\NodeController
}
/**
* GET /calendar/calendar/{id}
* GET /calendar/calendars/{id}
* Get specific calendar
*
* @param int $id ID Number
*
* @return \IPS\Api\PaginatedResponse<IPS\calendar\Calendar>
* @throws 1L364/1 INVALID_ID The calendar does not exist or the authorized user does not have permission to view it
* @return \IPS\calendar\Calendar
*/
public function GETitem( $id )
{
/* Return */
return $this->_view( $id );
try
{
return $this->_view( $id );
}
catch ( \OutOfRangeException $e )
{
throw new \IPS\Api\Exception( 'INVALID_ID', '1L364/1', 404 );
}
}
/**
* POST /calendar/calendar
* POST /calendar/calendars
* Create a calendar
*
* @apiclientonly
* @reqapiparam string title The calendar title
* @apiparam string color The calendar color (Hexadecimal)
* @apiparam int approve_events 0|1 Events must be approved?
@@ -65,18 +75,18 @@ class _calendars extends \IPS\Node\Api\NodeController
* @apiparam int approve_comments 0|1 Comments must be approved
* @apiparam int allow_reviews 0|1 Allow reviews
* @apiparam int approve_reviews 0|1 Reviews must be approved
*
* @return \IPS\calendar\Calendar
*/
public function POSTindex()
{
return $this->_create();
return new \IPS\Api\Response( 201, $this->_create()->apiOutput( $this->member ) );
}
/**
* POST /calendar/calendar/{id}
* POST /calendar/calendars/{id}
* Edit a calendar
*
* @apiclientonly
* @reqapiparam string title The calendar title
* @apiparam string color The calendar color (Hexadecimal)
* @apiparam int approve_events 0|1 Events must be approved?
@@ -84,22 +94,21 @@ class _calendars extends \IPS\Node\Api\NodeController
* @apiparam int approve_comments 0|1 Comments must be approved
* @apiparam int allow_reviews 0|1 Allow reviews
* @apiparam int approve_reviews 0|1 Reviews must be approved
*
* @return \IPS\calendar\Calendar
*/
public function POSTitem( $id )
{
$class = static::$class;
$class = $this->class;
$calendar = $class::load( $id );
$calendar = $this->_createOrUpdate( $calendar );
return $calendar;
return new \IPS\Api\Response( 201, $this->_createOrUpdate( $calendar )->apiOutput( $this->member ) );
}
/**
* DELETE /calendar/calendar/{id}
* DELETE /calendar/calendars/{id}
* Delete a calendar
*
* @apiclientonly
* @param int $id ID Number
* @return void
*/
+60 -18
View File
@@ -32,6 +32,7 @@ class _comments extends \IPS\Content\Api\CommentController
* GET /calendar/comments
* Get list of comments
*
* @note For requests using an OAuth Access Token for a particular member, only comments the authorized user can view will be included
* @apiparam string calendars Comma-delimited list of calendar IDs
* @apiparam string authors Comma-delimited list of member IDs - if provided, only topics started by those members are returned
* @apiparam int locked If 1, only comments from events which are locked are returned, if 0 only unlocked
@@ -40,6 +41,7 @@ class _comments extends \IPS\Content\Api\CommentController
* @apiparam string sortBy What to sort by. Can be 'date', 'title' or leave unspecified for ID
* @apiparam string sortDir Sort direction. Can be 'asc' or 'desc' - defaults to 'asc'
* @apiparam int page Page number
* @apiparam int perPage Number of results per page - defaults to 25
* @return \IPS\Api\PaginatedResponse<IPS\calendar\Event\Comment>
*/
public function GETindex()
@@ -52,14 +54,24 @@ class _comments extends \IPS\Content\Api\CommentController
* View information about a specific comment
*
* @param int $id ID Number
* @throws 2L297/1 INVALID_ID The comment ID does not exist
* @throws 2L297/1 INVALID_ID The comment ID does not exist or the authorized user does not have permission to view it
* @return \IPS\calendar\Event\Comment
*/
public function GETitem( $id )
{
try
{
return new \IPS\Api\Response( 200, \IPS\calendar\Event\Comment::load( $id )->apiOutput() );
$class = $this->class;
if ( $this->member )
{
$object = $class::loadAndCheckPerms( $id, $this->member );
}
else
{
$object = $class::load( $id );
}
return new \IPS\Api\Response( 200, $object->apiOutput( $this->member ) );
}
catch ( \OutOfRangeException $e )
{
@@ -71,21 +83,23 @@ class _comments extends \IPS\Content\Api\CommentController
* POST /calendar/comments
* Create a comment
*
* @note For requests using an OAuth Access Token for a particular member, any parameters the user doesn't have permission to use are ignored (for example, hidden will only be honoured if the authentictaed user has permission to hide content).
* @reqapiparam int event The ID number of the event the comment is for
* @reqapiparam int author The ID number of the member making the comment (0 for guest)
* @reqapiparam int author The ID number of the member making the comment (0 for guest). Required for requests made using an API Key or the Client Credentials Grant Type. For requests using an OAuth Access Token for a particular member, that member will always be the author
* @apiparam string author_name If author is 0, the guest name that should be used
* @reqapiparam string content The comment content as HTML (e.g. "<p>This is a comment.</p>")
* @apiparam datetime date The date/time that should be used for the comment date. If not provided, will use the current date/time
* @apiparam string ip_address The IP address that should be stored for the comment. If not provided, will use the IP address from the API request
* @reqapiparam string content The comment content as HTML (e.g. "<p>This is a comment.</p>"). Will be sanatized for requests using an OAuth Access Token for a particular member; will be saved unaltered for requests made using an API Key or the Client Credentials Grant Type.
* @apiparam datetime date The date/time that should be used for the comment date. If not provided, will use the current date/time. Ignored for requests using an OAuth Access Token for a particular member
* @apiparam string ip_address The IP address that should be stored for the comment. If not provided, will use the IP address from the API request. Ignored for requests using an OAuth Access Token for a particular member
* @apiparam int hidden 0 = unhidden; 1 = hidden, pending moderator approval; -1 = hidden (as if hidden by a moderator)
* @throws 2L297/3 INVALID_ID The comment ID does not exist
* @throws 1L297/4 NO_AUTHOR The author ID does not exist
* @throws 1L297/5 NO_CONTENT No content was supplied
* @throws 2L297/8 NO_PERMISSION The authorized user does not have permission to comment on that event
* @return \IPS\calendar\Event\Comment
*/
public function POSTindex()
{
/* Get topic */
/* Get event */
try
{
$event = \IPS\calendar\Event::load( \IPS\Request::i()->event );
@@ -96,18 +110,29 @@ class _comments extends \IPS\Content\Api\CommentController
}
/* Get author */
if ( \IPS\Request::i()->author )
if ( $this->member )
{
$author = \IPS\Member::load( \IPS\Request::i()->author );
if ( !$author->member_id )
if ( !$event->canComment( $this->member ) )
{
throw new \IPS\Api\Exception( 'NO_AUTHOR', '1L297/4', 404 );
throw new \IPS\Api\Exception( 'NO_PERMISSION', '2L297/8', 403 );
}
$author = $this->member;
}
else
{
$author = new \IPS\Member;
$author->name = \IPS\Request::i()->author_name;
if ( \IPS\Request::i()->author )
{
$author = \IPS\Member::load( \IPS\Request::i()->author );
if ( !$author->member_id )
{
throw new \IPS\Api\Exception( 'NO_AUTHOR', '1L297/4', 404 );
}
}
else
{
$author = new \IPS\Member;
$author->name = \IPS\Request::i()->author_name;
}
}
/* Check we have a post */
@@ -124,13 +149,15 @@ class _comments extends \IPS\Content\Api\CommentController
* POST /calendar/comments/{id}
* Edit a comment
*
* @note For requests using an OAuth Access Token for a particular member, any parameters the user doesn't have permission to use are ignored (for example, hidden will only be honoured if the authentictaed user has permission to hide content).
* @param int $id ID Number
* @apiparam int author The ID number of the member making the comment (0 for guest)
* @apiparam int author The ID number of the member making the comment (0 for guest). Ignored for requests using an OAuth Access Token for a particular member.
* @apiparam string author_name If author is 0, the guest name that should be used
* @apiparam string content The comment content as HTML (e.g. "<p>This is a comment.</p>")
* @apiparam string content The comment content as HTML (e.g. "<p>This is a comment.</p>"). Will be sanatized for requests using an OAuth Access Token for a particular member; will be saved unaltered for requests made using an API Key or the Client Credentials Grant Type.
* @apiparam int hidden 1/0 indicating if the topic should be hidden
* @throws 2L297/6 INVALID_ID The comment ID does not exist
* @throws 2L297/6 INVALID_ID The comment ID does not exist or the authorized user does not have permission to view it
* @throws 1L297/7 NO_AUTHOR The author ID does not exist
* @throws 2L297/9 NO_PERMISSION The authorized user does not have permission to edit the comment
* @return \IPS\calendar\Event\Comment
*/
public function POSTitem( $id )
@@ -139,6 +166,14 @@ class _comments extends \IPS\Content\Api\CommentController
{
/* Load */
$comment = \IPS\calendar\Event\Comment::load( $id );
if ( $this->member and !$comment->canView( $this->member ) )
{
throw new \OutOfRangeException;
}
if ( $this->member and !$comment->canEdit( $this->member ) )
{
throw new \IPS\Api\Exception( 'NO_PERMISSION', '2L297/9', 403 );
}
/* Do it */
try
@@ -161,14 +196,21 @@ class _comments extends \IPS\Content\Api\CommentController
* Deletes a comment
*
* @param int $id ID Number
* @throws 2L297/2 INVALID_ID The comment ID does not exist
* @throws 2L297/2 INVALID_ID The comment ID does not exist
* @throws 2L297/A NO_PERMISSION The authorized user does not have permission to delete the comment
* @return void
*/
public function DELETEitem( $id )
{
try
{
\IPS\calendar\Event\Comment::load( $id )->delete();
$class = $this->class;
$object = $class::load( $id );
if ( $this->member and !$object->canDelete( $this->member ) )
{
throw new \IPS\Api\Exception( 'NO_PERMISSION', '2L297/A', 403 );
}
$object->delete();
return new \IPS\Api\Response( 200, NULL );
}
+95 -32
View File
@@ -32,6 +32,7 @@ class _events extends \IPS\Content\Api\ItemController
* GET /calendar/events
* Get list of events
*
* @note For requests using an OAuth Access Token for a particular member, only events the authorized user can view will be included
* @apiparam string calendars Comma-delimited list of calendar IDs
* @apiparam string authors Comma-delimited list of member IDs - if provided, only events started by those members are returned
* @apiparam int locked If 1, only events which are locked are returned, if 0 only unlocked
@@ -40,6 +41,7 @@ class _events extends \IPS\Content\Api\ItemController
* @apiparam string sortBy What to sort by. Can be 'date' for creation date, 'title' or leave unspecified for ID
* @apiparam string sortDir Sort direction. Can be 'asc' or 'desc' - defaults to 'asc'
* @apiparam int page Page number
* @apiparam int perPage Number of results per page - defaults to 25
* @return \IPS\Api\PaginatedResponse<IPS\calendar\Event>
*/
public function GETindex()
@@ -56,7 +58,7 @@ class _events extends \IPS\Content\Api\ItemController
* View information about a specific event
*
* @param int $id ID Number
* @throws 1F294/1 INVALID_ID The event ID does not exist
* @throws 1F294/1 INVALID_ID The event ID does not exist or the authorized user does not have permission to view it
* @return \IPS\calendar\Event
*/
public function GETitem( $id )
@@ -75,10 +77,11 @@ class _events extends \IPS\Content\Api\ItemController
* POST /calendar/events
* Create an event
*
* @note For requests using an OAuth Access Token for a particular member, any parameters the user doesn't have permission to use are ignored (for example, locked will only be honoured if the authentictaed user has permission to lock events).
* @reqapiparam int calendar The ID number of the calendar the event should be created in
* @reqapiparam int author The ID number of the member creating the event (0 for guest)
* @apiparam int author The ID number of the member creating the event (0 for guest). Required for requests made using an API Key or the Client Credentials Grant Type. For requests using an OAuth Access Token for a particular member, that member will always be the author
* @reqapiparam string title The event title
* @reqapiparam string description The description as HTML (e.g. "<p>This is an event.</p>")
* @reqapiparam string description The description as HTML (e.g. "<p>This is an event.</p>"). Will be sanatized for requests using an OAuth Access Token for a particular member; will be saved unaltered for requests made using an API Key or the Client Credentials Grant Type.
* @reqapiparam datetime start The event start date/time
* @apiparam datetime end The event end date/time
* @apiparam string recurrence If this event recurs, the ICS recurrence definition
@@ -87,8 +90,8 @@ class _events extends \IPS\Content\Api\ItemController
* @apiparam \IPS\GeoLocation location The location where the event is taking place
* @apiparam string prefix Prefix tag
* @apiparam string tags Comma-separated list of tags (do not include prefix)
* @apiparam datetime date The date/time that should be used for the event/post post date. If not provided, will use the current date/time
* @apiparam string ip_address The IP address that should be stored for the event/post. If not provided, will use the IP address from the API request
* @apiparam datetime date The date/time that should be used for the event/post post date. If not provided, will use the current date/time. Ignored for requests using an OAuth Access Token for a particular member.
* @apiparam string ip_address The IP address that should be stored for the event/post. If not provided, will use the IP address from the API request. Ignored for requests using an OAuth Access Token for a particular member.
* @apiparam int locked 1/0 indicating if the event should be locked
* @apiparam int hidden 0 = unhidden; 1 = hidden, pending moderator approval; -1 = hidden (as if hidden by a moderator)
* @apiparam int featured 1/0 indicating if the event should be featured
@@ -98,6 +101,7 @@ class _events extends \IPS\Content\Api\ItemController
* @throws 1L296/9 NO_DESC No description was supplied
* @throws 1L296/A INVALID_START The start date is invalid
* @throws 1L296/B INVALID_END The end date is invalid
* @throws 2L296/C NO_PERMISSION The authorized user does not have permission to create an event in that calendar
* @return \IPS\calendar\Event
*/
public function POSTindex()
@@ -113,17 +117,28 @@ class _events extends \IPS\Content\Api\ItemController
}
/* Get author */
if ( \IPS\Request::i()->author )
if ( $this->member )
{
$author = \IPS\Member::load( \IPS\Request::i()->author );
if ( !$author->member_id )
if ( !$calendar->can( 'add', $this->member ) )
{
throw new \IPS\Api\Exception( 'NO_AUTHOR', '1L296/7', 400 );
throw new \IPS\Api\Exception( 'NO_PERMISSION', '2L296/C', 403 );
}
$author = $this->member;
}
else
{
$author = new \IPS\Member;
if ( \IPS\Request::i()->author )
{
$author = \IPS\Member::load( \IPS\Request::i()->author );
if ( !$author->member_id )
{
throw new \IPS\Api\Exception( 'NO_AUTHOR', '1L296/7', 400 );
}
}
else
{
$author = new \IPS\Member;
}
}
/* Check we have a title and a description */
@@ -158,17 +173,18 @@ class _events extends \IPS\Content\Api\ItemController
}
/* Do it */
return new \IPS\Api\Response( 201, $this->_create( $calendar, $author )->apiOutput() );
return new \IPS\Api\Response( 201, $this->_create( $calendar, $author )->apiOutput( $this->member ) );
}
/**
* POST /calendar/events/{id}
* Edit an event
*
* @note For requests using an OAuth Access Token for a particular member, any parameters the user doesn't have permission to use are ignored (for example, locked will only be honoured if the authentictaed user has permission to lock topics).
* @reqapiparam int calendar The ID number of the calendar the event should be created in
* @reqapiparam int author The ID number of the member creating the event (0 for guest)
* @reqapiparam int author The ID number of the member creating the event (0 for guest). Ignored for requests using an OAuth Access Token for a particular member.
* @reqapiparam string title The event title
* @reqapiparam string description The description as HTML (e.g. "<p>This is an event.</p>")
* @reqapiparam string description The description as HTML (e.g. "<p>This is an event.</p>"). Will be sanatized for requests using an OAuth Access Token for a particular member; will be saved unaltered for requests made using an API Key or the Client Credentials Grant Type.
* @reqapiparam datetime start The event start date/time
* @apiparam datetime end The event end date/time
* @apiparam string recurrence If this event recurs, the ICS recurrence definition
@@ -177,16 +193,17 @@ class _events extends \IPS\Content\Api\ItemController
* @apiparam \IPS\GeoLocation location The location where the event is taking place
* @apiparam string prefix Prefix tag
* @apiparam string tags Comma-separated list of tags (do not include prefix)
* @apiparam datetime date The date/time that should be used for the event/post post date. If not provided, will use the current date/time
* @apiparam string ip_address The IP address that should be stored for the event/post. If not provided, will use the IP address from the API request
* @apiparam datetime date The date/time that should be used for the event/post post date. If not provided, will use the current date/time. Ignored for requests using an OAuth Access Token for a particular member.
* @apiparam string ip_address The IP address that should be stored for the event/post. If not provided, will use the IP address from the API request. Ignored for requests using an OAuth Access Token for a particular member.
* @apiparam int locked 1/0 indicating if the event should be locked
* @apiparam int hidden 0 = unhidden; 1 = hidden, pending moderator approval; -1 = hidden (as if hidden by a moderator)
* @apiparam int featured 1/0 indicating if the event should be featured
* @throws 1L296/I INVALID_ID The event ID is invalid
* @throws 1L296/D NO_CALENDAR The calendar ID does not exist
* @throws 1L296/I INVALID_ID The event ID is invalid or the authorized user does not have permission to view it
* @throws 1L296/D NO_CALENDAR The calendar ID does not exist or the authorized user does not have permission to post in it
* @throws 1L296/E NO_AUTHOR The author ID does not exist
* @throws 1L296/G INVALID_START The start date is invalid
* @throws 1L296/H INVALID_END The end date is invalid
* @throws 2L296/D NO_PERMISSION The authorized user does not have permission to edit the topic
* @return \IPS\calendar\Event
*/
public function POSTitem( $id )
@@ -194,13 +211,26 @@ class _events extends \IPS\Content\Api\ItemController
try
{
$event = \IPS\calendar\Event::load( $id );
if ( $this->member and !$event->can( 'read', $this->member ) )
{
throw new \OutOfRangeException;
}
if ( $this->member and !$event->canEdit( $this->member ) )
{
throw new \IPS\Api\Exception( 'NO_PERMISSION', '2L296/D', 403 );
}
/* New calendar */
if ( isset( \IPS\Request::i()->calendar ) and \IPS\Request::i()->calendar != $event->forum_id )
if ( isset( \IPS\Request::i()->calendar ) and \IPS\Request::i()->calendar != $event->calendar_id and ( !$this->member or $event->canMove( $this->member ) ) )
{
try
{
$newCalendar = \IPS\calendar\Calendar::load( \IPS\Request::i()->calendar );
if ( $this->member and !$newCalendar->can( 'add', $this->member ) )
{
throw new \OutOfRangeException;
}
$event->move( $newCalendar );
}
catch ( \OutOfRangeException $e )
@@ -210,7 +240,7 @@ class _events extends \IPS\Content\Api\ItemController
}
/* New author */
if ( isset( \IPS\Request::i()->author ) )
if ( !$this->member and isset( \IPS\Request::i()->author ) )
{
try
{
@@ -253,11 +283,11 @@ class _events extends \IPS\Content\Api\ItemController
}
/* Everything else */
$this->_createOrUpdate( $event );
$this->_createOrUpdate( $event, 'edit' );
/* Save and return */
$event->save();
return new \IPS\Api\Response( 200, $event->apiOutput() );
return new \IPS\Api\Response( 200, $event->apiOutput( $this->member ) );
}
catch ( \OutOfRangeException $e )
{
@@ -273,7 +303,8 @@ class _events extends \IPS\Content\Api\ItemController
* @apiparam int hidden If 1, only comments which are hidden are returned, if 0 only not hidden
* @apiparam string sortDir Sort direction. Can be 'asc' or 'desc' - defaults to 'asc'
* @apiparam int page Page number
* @throws 2L296/2 INVALID_ID The event ID does not exist
* @apiparam int perPage Number of results per page - defaults to 25
* @throws 2L296/2 INVALID_ID The event ID does not exist or the authorized user does not have permission to view it
* @return \IPS\Api\PaginatedResponse<IPS\calendar\Event\Comment>
*/
public function GETitem_comments( $id )
@@ -296,7 +327,8 @@ class _events extends \IPS\Content\Api\ItemController
* @apiparam int hidden If 1, only comments which are hidden are returned, if 0 only not hidden
* @apiparam string sortDir Sort direction. Can be 'asc' or 'desc' - defaults to 'asc'
* @apiparam int page Page number
* @throws 2L296/3 INVALID_ID The event ID does not exist
* @apiparam int perPage Number of results per page - defaults to 25
* @throws 2L296/3 INVALID_ID The event ID does not exist or the authorized user does not have permission to view it
* @return \IPS\Api\PaginatedResponse<IPS\calendar\Event\Review>
*/
public function GETitem_reviews( $id )
@@ -316,7 +348,7 @@ class _events extends \IPS\Content\Api\ItemController
* Get RSVPs on an event
*
* @param int $id ID Number
* @throws 2L296/3 INVALID_ID The event ID does not exist
* @throws 2L296/3 INVALID_ID The event ID does not exist or the authorized user does not have permission to view it
* @return array
* @apiresponse [\IPS\Member] attending Members that have confirmed they are attending the event
* @apiresponse [\IPS\Member] notAttending Members that have confirmed they are not attending the event
@@ -327,16 +359,21 @@ class _events extends \IPS\Content\Api\ItemController
try
{
$event = \IPS\calendar\Event::load( $id );
if ( $this->member and !$event->can( 'read', $this->member ) )
{
throw new \OutOfRangeException;
}
$attendees = $event->attendees();
return new \IPS\Api\Response( 200, array(
'attending' => array_values( array_map( function( $member ) {
return $member->apiOutput();
return $member->apiOutput( $this->member );
}, $attendees[1] ) ),
'notAttending' => array_values( array_map( function( $member ) {
return $member->apiOutput();
return $member->apiOutput( $this->member );
}, $attendees[0] ) ),
'maybeAttending' => array_values( array_map( function( $member ) {
return $member->apiOutput();
return $member->apiOutput( $this->member );
}, $attendees[2] ) ),
) );
}
@@ -350,10 +387,12 @@ class _events extends \IPS\Content\Api\ItemController
* PUT /calendar/events/{id}/rsvps/{member_id}
* RSVP a member to an event
*
* @note For requests using an OAuth Access Token for a particular member, the member ID must be the authorized member's ID
* @reqapiparam int response 0 = Not attending; 1 = attending; 2 = maybe attending
* @param int $id Event ID NUmber
* @param int $memberId Member ID NUmber
* @throws 2L296/J INVALID_ID The event ID does not exist
* @throws 2L296/K INVALID_MEMBER The member ID was not valid
* @return void
*/
public function PUTitem_rsvps( $id, $memberId )
@@ -363,6 +402,11 @@ class _events extends \IPS\Content\Api\ItemController
throw new \IPS\Api\Exception( 'INVALID_RESPONSE', '1L296/L', 400 );
}
if ( $this->member and $memberId != $this->member->member_id )
{
throw new \IPS\Api\Exception( 'INVALID_MEMBER', '2L296/K', 404 );
}
try
{
$event = \IPS\calendar\Event::load( $id );
@@ -401,12 +445,19 @@ class _events extends \IPS\Content\Api\ItemController
* DELETE /calendar/events/{id}/rsvps/{member_id}
* Remove a member from RSVP list
*
* @note For requests using an OAuth Access Token for a particular member, the member ID must be the authorized member's ID
* @param int $id Event ID NUmber
* @param int $memberId Member ID NUmber
* @return void
* @throws 2L296/K INVALID_MEMBER The member ID was not valid
*/
public function DELETEitem_rsvps( $id, $memberId )
{
if ( $this->member and $memberId != $this->member->member_id )
{
throw new \IPS\Api\Exception( 'INVALID_MEMBER', '2L296/K', 404 );
}
\IPS\Db::i()->delete( 'calendar_event_rsvp', array( 'rsvp_event_id=? AND rsvp_member_id=?', intval( $id ), intval( $memberId ) ) );
return new \IPS\Api\Response( 200, NULL );
}
@@ -415,9 +466,10 @@ class _events extends \IPS\Content\Api\ItemController
* Create or update event
*
* @param \IPS\Content\Item $item The item
* @param string $type add or edit
* @return \IPS\Content\Item
*/
protected function _createOrUpdate( \IPS\Content\Item $item )
protected function _createOrUpdate( \IPS\Content\Item $item, $type='add' )
{
/* Start/End date */
$startDate = new \IPS\DateTime( \IPS\Request::i()->start );
@@ -440,10 +492,15 @@ class _events extends \IPS\Content\Api\ItemController
}
/* Description */
$item->content = \IPS\Request::i()->description;
$descriptionContents = \IPS\Request::i()->description;
if ( $this->member )
{
$descriptionContents = \IPS\Text\Parser::parseStatic( $descriptionContents, TRUE, NULL, $this->member, 'calendar_Calendar' );
}
$item->content = $descriptionContents;
/* RSVP */
if ( isset( \IPS\Request::i()->rsvp ) )
if ( isset( \IPS\Request::i()->rsvp ) and ( !$this->member or $item->container()->can( 'askrsvp', $this->member ) ) )
{
$item->rsvp = intval( \IPS\Request::i()->rsvp );
if ( $item->rsvp and isset( \IPS\Request::i()->rsvpLimit ) and \IPS\Request::i()->rsvpLimit )
@@ -475,7 +532,7 @@ class _events extends \IPS\Content\Api\ItemController
}
/* Pass up */
return parent::_createOrUpdate( $item );
return parent::_createOrUpdate( $item, $type );
}
/**
@@ -490,7 +547,13 @@ class _events extends \IPS\Content\Api\ItemController
{
try
{
\IPS\calendar\Event::load( $id )->delete();
$item = \IPS\calendar\Event::load( $id );
if ( $this->member and !$item->canDelete( $this->member ) )
{
throw new \IPS\Api\Exception( 'NO_PERMISSION', '2F294/B', 404 );
}
$item->delete();
return new \IPS\Api\Response( 200, NULL );
}
+60 -18
View File
@@ -32,6 +32,7 @@ class _reviews extends \IPS\Content\Api\CommentController
* GET /calendar/reviews
* Get list of reviews
*
* @note For requests using an OAuth Access Token for a particular member, only reviews the authorized user can view will be included
* @apiparam string calendars Comma-delimited list of calendar IDs
* @apiparam string authors Comma-delimited list of member IDs - if provided, only topics started by those members are returned
* @apiparam int locked If 1, only reviews from events which are locked are returned, if 0 only unlocked
@@ -40,6 +41,7 @@ class _reviews extends \IPS\Content\Api\CommentController
* @apiparam string sortBy What to sort by. Can be 'date', 'title' or leave unspecified for ID
* @apiparam string sortDir Sort direction. Can be 'asc' or 'desc' - defaults to 'asc'
* @apiparam int page Page number
* @apiparam int perPage Number of results per page - defaults to 25
* @return \IPS\Api\PaginatedResponse<IPS\calendar\Event\Review>
*/
public function GETindex()
@@ -52,14 +54,24 @@ class _reviews extends \IPS\Content\Api\CommentController
* View information about a specific review
*
* @param int $id ID Number
* @throws 2L298/1 INVALID_ID The review ID does not exist
* @throws 2L298/1 INVALID_ID The review ID does not exist or the authorized user does not have permission to view it
* @return \IPS\calendar\Event\Review
*/
public function GETitem( $id )
{
try
{
return new \IPS\Api\Response( 200, \IPS\calendar\Event\Review::load( $id )->apiOutput() );
$class = $this->class;
if ( $this->member )
{
$object = $class::loadAndCheckPerms( $id, $this->member );
}
else
{
$object = $class::load( $id );
}
return new \IPS\Api\Response( 200, $object->apiOutput( $this->member ) );
}
catch ( \OutOfRangeException $e )
{
@@ -71,23 +83,25 @@ class _reviews extends \IPS\Content\Api\CommentController
* POST /calendar/reviews
* Create a review
*
* @note For requests using an OAuth Access Token for a particular member, any parameters the user doesn't have permission to use are ignored (for example, hidden will only be honoured if the authentictaed user has permission to hide content).
* @reqapiparam int event The ID number of the event the review is for
* @reqapiparam int author The ID number of the member making the review (0 for guest)
* @reqapiparam int author The ID number of the member making the review (0 for guest). Required for requests made using an API Key or the Client Credentials Grant Type. For requests using an OAuth Access Token for a particular member, that member will always be the author
* @apiparam string author_name If author is 0, the guest name that should be used
* @reqapiparam string content The review content as HTML (e.g. "<p>This is a review.</p>")
* @apiparam datetime date The date/time that should be used for the review date. If not provided, will use the current date/time
* @apiparam string ip_address The IP address that should be stored for the review. If not provided, will use the IP address from the API request
* @reqapiparam string content The review content as HTML (e.g. "<p>This is a review.</p>"). Will be sanatized for requests using an OAuth Access Token for a particular member; will be saved unaltered for requests made using an API Key or the Client Credentials Grant Type.
* @apiparam datetime date The date/time that should be used for the review date. If not provided, will use the current date/time. Ignored for requests using an OAuth Access Token for a particular member
* @apiparam string ip_address The IP address that should be stored for the review. If not provided, will use the IP address from the API request. Ignored for requests using an OAuth Access Token for a particular member
* @apiparam int hidden 0 = unhidden; 1 = hidden, pending moderator approval; -1 = hidden (as if hidden by a moderator)
* @reqapiparam int rating Star rating
* @throws 2L298/2 INVALID_ID The forum ID does not exist
* @throws 1L298/3 NO_AUTHOR The author ID does not exist
* @throws 1L298/4 NO_CONTENT No content was supplied
* @throws 1L298/8 INVALID_RATING The rating is not a valid number up to the maximum rating
* @throws 2L298/A NO_PERMISSION The authorized user does not have permission to review that event
* @return \IPS\calendar\Event\Review
*/
public function POSTindex()
{
/* Get topic */
/* Get event */
try
{
$event = \IPS\calendar\Event::load( \IPS\Request::i()->event );
@@ -98,18 +112,29 @@ class _reviews extends \IPS\Content\Api\CommentController
}
/* Get author */
if ( \IPS\Request::i()->author )
if ( $this->member )
{
$author = \IPS\Member::load( \IPS\Request::i()->author );
if ( !$author->member_id )
if ( !$event->canReview( $this->member ) )
{
throw new \IPS\Api\Exception( 'NO_AUTHOR', '1L298/3', 404 );
throw new \IPS\Api\Exception( 'NO_PERMISSION', '2L298/A', 403 );
}
$author = $this->member;
}
else
{
$author = new \IPS\Member;
$author->name = \IPS\Request::i()->author_name;
if ( \IPS\Request::i()->author )
{
$author = \IPS\Member::load( \IPS\Request::i()->author );
if ( !$author->member_id )
{
throw new \IPS\Api\Exception( 'NO_AUTHOR', '1L298/3', 404 );
}
}
else
{
$author = new \IPS\Member;
$author->name = \IPS\Request::i()->author_name;
}
}
/* Check we have a post */
@@ -132,15 +157,17 @@ class _reviews extends \IPS\Content\Api\CommentController
* POST /calendar/reviews/{id}
* Edit a review
*
* @note For requests using an OAuth Access Token for a particular member, any parameters the user doesn't have permission to use are ignored (for example, hidden will only be honoured if the authentictaed user has permission to hide content).
* @param int $id ID Number
* @apiparam int author The ID number of the member making the review (0 for guest)
* @apiparam int author The ID number of the member making the review (0 for guest). Ignored for requests using an OAuth Access Token for a particular member.
* @apiparam string author_name If author is 0, the guest name that should be used
* @apiparam string content The review content as HTML (e.g. "<p>This is a review.</p>")
* @apiparam string content The review content as HTML (e.g. "<p>This is a review.</p>"). Will be sanatized for requests using an OAuth Access Token for a particular member; will be saved unaltered for requests made using an API Key or the Client Credentials Grant Type.
* @apiparam int hidden 1/0 indicating if the topic should be hidden
* @apiparam int rating Star rating
* @throws 2L298/5 INVALID_ID The review ID does not exist
* @throws 2L298/5 INVALID_ID The review ID does not exist or the authorized user does not have permission to view it
* @throws 1L298/6 NO_AUTHOR The author ID does not exist
* @throws 1L298/9 INVALID_RATING The rating is not a valid number up to the maximum rating
* @throws 2L298/B NO_PERMISSION The authorized user does not have permission to edit the review
* @return \IPS\calendar\Event\Review
*/
public function POSTitem( $id )
@@ -149,6 +176,14 @@ class _reviews extends \IPS\Content\Api\CommentController
{
/* Load */
$comment = \IPS\calendar\Event\Review::load( $id );
if ( $this->member and !$comment->canView( $this->member ) )
{
throw new \OutOfRangeException;
}
if ( $this->member and !$comment->canEdit( $this->member ) )
{
throw new \IPS\Api\Exception( 'NO_PERMISSION', '2L298/B', 403 );
}
/* Check */
if ( isset( \IPS\Request::i()->rating ) and !in_array( (int) \IPS\Request::i()->rating, range( 1, \IPS\Settings::i()->reviews_rating_out_of ) ) )
@@ -176,14 +211,21 @@ class _reviews extends \IPS\Content\Api\CommentController
* Deletes a review
*
* @param int $id ID Number
* @throws 2L298/7 INVALID_ID The review ID does not exist
* @throws 2L298/7 INVALID_ID The review ID does not exist
* @throws 2L298/C NO_PERMISSION The authorized user does not have permission to delete the comment
* @return void
*/
public function DELETEitem( $id )
{
try
{
\IPS\calendar\Event\Review::load( $id )->delete();
$class = $this->class;
$object = $class::load( $id );
if ( $this->member and !$object->canDelete( $this->member ) )
{
throw new \IPS\Api\Exception( 'NO_PERMISSION', '2L298/C', 403 );
}
$object->delete();
return new \IPS\Api\Response( 200, NULL );
}
+4 -4
View File
@@ -56,7 +56,7 @@
{$email->language->addToStack("go_to_this_x", FALSE, array( 'sprintf' => array( mb_strtolower( $email->language->addToStack( $content::$title, FALSE ) ) ) ) )}: {$content->url()}
-- {setting="board_name"}]]></template_content_plaintext></template><template><template_app>calendar</template_app><template_name>notification_new_content</template_name><template_content_html><![CDATA[
-- {setting="board_name"}]]></template_content_plaintext><template_pinned>0</template_pinned></template><template><template_app>calendar</template_app><template_name>notification_new_content</template_name><template_content_html><![CDATA[
{{if $content->author()->member_id}}
{$email->language->addToStack("email_new_calevent", FALSE, array( 'sprintf' => array( $content->author()->url(), $content->author()->name, $content->indefiniteArticle( $email->language ), $content->url(), $content->mapped('title'), $content->fixedDateTimeDescription( $email->language ) ) ) )}
{{else}}
@@ -107,7 +107,7 @@
{$email->language->addToStack("go_to_this_x", FALSE, array( 'sprintf' => array( mb_strtolower( $email->language->addToStack( $content::$title, FALSE ) ) ) ) )}: {$content->url()}
-- {setting="board_name"}]]></template_content_plaintext></template><template><template_app>calendar</template_app><template_name>digests__item</template_name><template_content_html><![CDATA[<tr style='border-bottom: 1px solid #eeeeee'>
-- {setting="board_name"}]]></template_content_plaintext><template_pinned>0</template_pinned></template><template><template_app>calendar</template_app><template_name>digests__item</template_name><template_content_html><![CDATA[<tr style='border-bottom: 1px solid #eeeeee'>
<td dir='{dir}' width='120' valign='top' align='center'>
<table width='120' cellpadding='10' cellspacing='0' border='0' bgcolor='#fafafa'>
<tr>
@@ -181,7 +181,7 @@
{{endif}}
</td>
</tr>]]></template_content_html><template_data>$event, $email</template_data><template_content_plaintext><![CDATA[{$email->language->addToStack("x_created_event_in", FALSE, array( 'sprintf' => array( $event->author()->name, $event->container()->_title ) ) )}: {$event->title} ({$event->url()})
]]></template_content_plaintext></template><template><template_app>calendar</template_app><template_name>digests__comment</template_name><template_content_html><![CDATA[<tr style='border-bottom: 1px solid #eeeeee'>
]]></template_content_plaintext><template_pinned>0</template_pinned></template><template><template_app>calendar</template_app><template_name>digests__comment</template_name><template_content_html><![CDATA[<tr style='border-bottom: 1px solid #eeeeee'>
<td dir='{dir}' width='120' valign='top' align='center'>
<table width='120' cellpadding='10' cellspacing='0' border='0' bgcolor='#fafafa'>
<tr>
@@ -222,4 +222,4 @@
</td>
</tr>]]></template_content_html><template_data>$comment, $email</template_data><template_content_plaintext><![CDATA[{$email->language->addToStack("x_commented_event", FALSE, array( 'sprintf' => array( $comment->author()->name ) ) )}: {$comment->item()->name} ({$comment->url()})
]]></template_content_plaintext></template></emails>
]]></template_content_plaintext><template_pinned>0</template_pinned></template></emails>
+44 -9
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<javascript app="calendar">
<file javascript_app="calendar" javascript_location="front" javascript_path="controllers/browse" javascript_name="ips.browse.main.js" javascript_type="controller" javascript_version="102002" javascript_position="1000050"><![CDATA[/**
<file javascript_app="calendar" javascript_location="front" javascript_path="controllers/browse" javascript_name="ips.browse.main.js" javascript_type="controller" javascript_version="103005" javascript_position="1000250"><![CDATA[/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
@@ -112,7 +112,7 @@
}
});
}(jQuery, _));]]></file>
<file javascript_app="calendar" javascript_location="front" javascript_path="controllers/browse" javascript_name="ips.browse.monthView.js" javascript_type="controller" javascript_version="102002" javascript_position="1000050"><![CDATA[/**
<file javascript_app="calendar" javascript_location="front" javascript_path="controllers/browse" javascript_name="ips.browse.monthView.js" javascript_type="controller" javascript_version="103005" javascript_position="1000250"><![CDATA[/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
@@ -254,7 +254,7 @@
}
});
}(jQuery, _));]]></file>
<file javascript_app="calendar" javascript_location="front" javascript_path="controllers/submit" javascript_name="ips.submit.dates.js" javascript_type="controller" javascript_version="102002" javascript_position="1000100"><![CDATA[/**
<file javascript_app="calendar" javascript_location="front" javascript_path="controllers/submit" javascript_name="ips.submit.dates.js" javascript_type="controller" javascript_version="103005" javascript_position="1000100"><![CDATA[/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
@@ -278,6 +278,7 @@
this.on( 'change', 'input, select', this.checkForSummaryChange );
this.on( 'change', '#check_single_day, #check_all_day', this.toggleFields );
this.on( 'change', '#check_no_end_time', this.toggleEndtimeFields );
this.on( 'click', '#elRecurRemove', this.disableRecurring );
this.on( 'change', 'input[name="event_dates[start_date]"]', this.setEndDateOnStartChange );
this.on( 'change', '#event_end_date', this.setHasFocusedEventOnEndChange );
@@ -372,6 +373,23 @@
this._resizeEndGrid();
},
/**
* Toggle the end time enabled/disabled status
*
* @param {event} e Event object
* @returns {void}
*/
toggleEndtimeFields: function(e) {
if( this.scope.find('#check_no_end_time').is(':checked') )
{
this.scope.find('#end_time').prop('disabled', true);
}
else
{
this.scope.find('#end_time').prop('disabled', false);
}
},
/**
* Manual toggle functionality for the Single/All Day checkboxes
*
@@ -379,6 +397,8 @@
* @returns {void}
*/
toggleFields: function (e) {
this.toggleEndtimeFields();
var singleDay = this.scope.find('#check_single_day');
var allDay = this.scope.find('#check_all_day');
var self = this;
@@ -388,7 +408,8 @@
end_time_wrap: true,
event_end_date_wrap: true,
elDateGrid_arrow: true,
elDateGrid_end: true
elDateGrid_end: true,
end_date_controls: true
};
// Single day, not all day
@@ -403,6 +424,13 @@
} else if( !singleDay.is(':checked') && allDay.is(':checked') ){
toggles.start_time_wrap = false;
toggles.end_time_wrap = false;
toggles.end_date_controls = false;
}
// Multiple days, not all day
else
{
toggles.end_date_controls = false;
this.scope.find('#end_time').prop('disabled', false);
}
// Hide appropriate elements
@@ -530,7 +558,14 @@
var finalString = '';
if( singleDay.is(':checked') && !allDay.is(':checked') ){
finalString = ips.getString( 'single_not_allday', { startDate: startDateString, startTime: startTime, endTime: endTime } );
if( this.scope.find('#check_no_end_time').is(':checked') )
{
finalString = ips.getString( 'single_not_allday_noendtime', { startDate: startDateString, startTime: startTime } );
}
else
{
finalString = ips.getString( 'single_not_allday', { startDate: startDateString, startTime: startTime, endTime: endTime } );
}
} else if ( ( !singleDay.is(':checked') && !allDay.is(':checked') ) && endDateString && startTime && endTime ) {
finalString = ips.getString( 'not_single_not_allday', { startDate: startDateString, endDate: endDateString, startTime: startTime, endTime: endTime } );
} else if( !singleDay.is(':checked') && allDay.is(':checked') && endDateString ) {
@@ -664,7 +699,7 @@
}
});
}(jQuery, _));]]></file>
<file javascript_app="calendar" javascript_location="front" javascript_path="controllers/venue" javascript_name="ips.venue.main.js" javascript_type="controller" javascript_version="102002" javascript_position="1000150"><![CDATA[/**
<file javascript_app="calendar" javascript_location="front" javascript_path="controllers/venue" javascript_name="ips.venue.main.js" javascript_type="controller" javascript_version="103005" javascript_position="1000200"><![CDATA[/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
@@ -776,7 +811,7 @@
}
});
}(jQuery, _));]]></file>
<file javascript_app="calendar" javascript_location="front" javascript_path="controllers/view" javascript_name="ips.view.reminderButton.js" javascript_type="controller" javascript_version="102002" javascript_position="1000200"><![CDATA[/**
<file javascript_app="calendar" javascript_location="front" javascript_path="controllers/view" javascript_name="ips.view.reminderButton.js" javascript_type="controller" javascript_version="103005" javascript_position="1000150"><![CDATA[/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
@@ -859,7 +894,7 @@
}
});
}(jQuery, _));]]></file>
<file javascript_app="calendar" javascript_location="front" javascript_path="controllers/view" javascript_name="ips.view.reminderForm.js" javascript_type="controller" javascript_version="102002" javascript_position="1000200">/**
<file javascript_app="calendar" javascript_location="front" javascript_path="controllers/view" javascript_name="ips.view.reminderForm.js" javascript_type="controller" javascript_version="103005" javascript_position="1000150">/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
@@ -969,7 +1004,7 @@
});
}(jQuery, _));
</file>
<file javascript_app="calendar" javascript_location="front" javascript_path="templates" javascript_name="ips.calendar.templates.js" javascript_type="framework" javascript_version="102002" javascript_position="1000250"><![CDATA[/**
<file javascript_app="calendar" javascript_location="front" javascript_path="templates" javascript_name="ips.calendar.templates.js" javascript_type="framework" javascript_version="103005" javascript_position="1000050"><![CDATA[/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*/
+6 -3
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<language>
<app key="calendar" version="102002">
<app key="calendar" version="103005">
<word key="__app_calendar" js="0">Calendar</word>
<word key="frontnavigation_calendar" js="0">Calendar</word>
<word key="__indefart_calendar_event_comment" js="0">a comment on a calendar event</word>
@@ -64,7 +64,7 @@
<word key="calendar_custom_df" js="0">Or specify a custom date format</word>
<word key="calendar_date_format" js="0">Date format for events</word>
<word key="calendar_date_format_custom" js="0">Custom date format</word>
<word key="calendar_date_format_custom_desc" js="0"><![CDATA[You can use any <a href='http://php.net/strftime' target='_blank'>PHP strftime</a> options to specify a custom format for dates in calendar]]></word>
<word key="calendar_date_format_custom_desc" js="0"><![CDATA[You can use any <a href='{external.php-strftime}' target='_blank'>PHP strftime</a> options to specify a custom format for dates in calendar]]></word>
<word key="calendar_no_date_format" js="0">You must either choose a predefined date format or specify a custom date format</word>
<word key="calendars" js="0">Calendars</word>
<word key="calendars_sg" js="0">Calendar</word>
@@ -95,7 +95,7 @@
<word key="can_feature_calendar_event" js="0">Can feature events?</word>
<word key="can_unfeature_calendar_event" js="0">Can unfeature events?</word>
<word key="can_move_calendar_event" js="0">Can move events?</word>
<word key="can_move_calendar_event_desc" js="0">Will be able to move events only between the calendars they have permission to moderate, not to any other calendar.</word>
<word key="can_move_calendar_event_desc" js="0">Will be able to move events only between the calendars they can create new content within.</word>
<word key="can_lock_calendar_event" js="0">Can lock events?</word>
<word key="can_unlock_calendar_event" js="0">Can unlock events?</word>
<word key="can_reply_to_locked_calendar_event" js="0">Can comment on locked events?</word>
@@ -204,6 +204,7 @@
<word key="event_rsvp_desc" js="0">Members who are allowed to RSVP for events will be asked to do so and will be able to see the list of members who have RSVP'd for this event. For recurring events, responses apply to all occurrences.</word>
<word key="event_to" js="0">to</word>
<word key="event_all_day" js="0">This is an all day event</word>
<word key="no_end_time" js="0">There is no end time</word>
<word key="event_repeat" js="0">Repeat</word>
<word key="change_timezone" js="0">Change the time zone</word>
<word key="event_timezone" js="0">Event Time Zone</word>
@@ -432,6 +433,7 @@
<word key="menu__calendar_calendars_venues" js="0">Venues</word>
<word key="venues" js="0">Venues</word>
<word key="venue_title" js="0">Venue Name</word>
<word key="event_venue_name" js="0">Event Venue</word>
<word key="event_venue" js="0">Venue</word>
<word key="venues_not_listed" js="0">Venue not listed</word>
<word key="venue_address" js="0">Address</word>
@@ -467,6 +469,7 @@
<word key="week_string" js="1">{{week}} {{days}}</word>
<word key="doesnt_repeat" js="1">Doesn't repeat</word>
<word key="single_not_allday" js="1"><![CDATA[On <strong>{{startDate}}</strong>, from <strong>{{startTime}}</strong> to <strong>{{endTime}}</strong>]]></word>
<word key="single_not_allday_noendtime" js="1"><![CDATA[On <strong>{{startDate}}</strong>, at <strong>{{startTime}}</strong>]]></word>
<word key="single_allday" js="1"><![CDATA[<strong>{{startDate}}</strong> (all day)]]></word>
<word key="not_single_not_allday" js="1"><![CDATA[From <strong>{{startDate}}</strong> at <strong>{{startTime}}</strong> until <strong>{{endDate}}</strong> at <strong>{{endTime}}</strong>]]></word>
<word key="not_single_allday" js="1"><![CDATA[From <strong>{{startDate}}</strong> to <strong>{{endDate}}</strong>]]></word>
+67 -11
View File
@@ -181,7 +181,9 @@
"cal_allow_reviews": "0",
"cal_review_moderate": "0"
}
]
],
"comment": "",
"reporting": "count"
},
"calendar_event_comments": {
"name": "calendar_event_comments",
@@ -392,7 +394,9 @@
null
]
}
}
},
"comment": "",
"reporting": "count"
},
"calendar_event_rsvp": {
"name": "calendar_event_rsvp",
@@ -498,8 +502,22 @@
"columns": [
"rsvp_member_id"
]
},
"rsvp_search": {
"type": "key",
"name": "rsvp_search",
"columns": [
"rsvp_date",
"rsvp_member_id"
],
"length": [
null,
null
]
}
}
},
"comment": "",
"reporting": "count"
},
"calendar_events": {
"name": "calendar_events",
@@ -1111,10 +1129,26 @@
"length": [
null
]
},
"event_album": {
"type": "key",
"name": "event_album",
"columns": [
"event_album",
"event_approved",
"event_saved"
],
"length": [
null,
null,
null
]
}
},
"collation": "utf8mb4_unicode_ci",
"engine": "InnoDB"
"engine": "InnoDB",
"comment": "",
"reporting": "count"
},
"calendar_import_feeds": {
"name": "calendar_import_feeds",
@@ -1291,7 +1325,9 @@
"feed_last_run"
]
}
}
},
"comment": "",
"reporting": "count"
},
"calendar_import_map": {
"name": "calendar_import_map",
@@ -1609,6 +1645,20 @@
"unsigned": false,
"values": [],
"zerofill": false
},
"review_author_response": {
"name": "review_author_response",
"type": "MEDIUMTEXT",
"length": null,
"decimals": null,
"values": null,
"allow_null": true,
"default": null,
"comment": "",
"unsigned": false,
"zerofill": false,
"auto_increment": false,
"binary": false
}
},
"indexes": {
@@ -1635,11 +1685,13 @@
"review_mid": {
"type": "key",
"name": "review_mid",
"length": [
null
],
"columns": [
"review_mid"
"review_mid",
"review_date"
],
"length": [
null,
null
]
},
"review_ip": {
@@ -1662,7 +1714,9 @@
null
]
}
}
},
"comment": "",
"reporting": "count"
},
"calendar_event_reminders": {
"name": "calendar_event_reminders",
@@ -1858,6 +1912,8 @@
"venue_id"
]
}
}
},
"comment": "",
"reporting": "count"
}
}
+18 -9
View File
@@ -1,38 +1,47 @@
[
{
"key": "all_activity_calendar_streamitems",
"default": "1"
"default": "1",
"report": "full"
},
{
"key": "calendar_date_format",
"default": "d_lm_y"
"default": "d_lm_y",
"report": "full"
},
{
"key": "calendar_date_format_custom",
"default": ""
"default": "",
"report": "bool"
},
{
"key": "calendar_default_view",
"default": "month"
"default": "month",
"report": "full"
},
{
"key": "calendar_rss_feed",
"default": "1"
"default": "1",
"report": "full"
},
{
"key": "calendar_rss_feed_days",
"default": "30"
"default": "30",
"report": "full"
},
{
"key": "calendar_venues_enabled",
"default": "0"
"default": "0",
"report": "full"
},
{
"key": "ipb_calendar_mon",
"default": "1"
"default": "1",
"report": "full"
},
{
"key": "show_bday_calendar",
"default": "1"
"default": "1",
"report": "full"
}
]
+168 -99
View File
@@ -167,13 +167,15 @@
<h2 class='ipsType_reset ipsType_sectionTitle'>{lang="featured_events"}</h2>
<div class='ipsPad'>
<div class='ipsCarousel ipsClearfix' data-ipsCarousel>
<ul class='cCalendarCarousel ipsClearfix' data-role="carouselItems">
{{foreach $featured as $event}}
<li class='ipsCarousel_item'>
{template="eventBlock" app="calendar" group="view" params="$event, $date, true, false"}
</li>
{{endforeach}}
</ul>
<div class='ipsCarousel_inner'>
<ul class='cCalendarCarousel ipsClearfix' data-role="carouselItems">
{{foreach $featured as $event}}
<li class='ipsCarousel_item'>
{template="eventBlock" app="calendar" group="view" params="$event, $date, true, false"}
</li>
{{endforeach}}
</ul>
</div>
<span class='ipsCarousel_shadow ipsCarousel_shadowLeft'></span>
<span class='ipsCarousel_shadow ipsCarousel_shadowRight'></span>
<a href='#' class='ipsCarousel_nav ipsHide' data-action='prev'><i class='fa fa-chevron-left'></i></a>
@@ -219,7 +221,11 @@
<ul class='ipsMenu ipsMenu_auto ipsHide' id='elCalendars_menu'>
<li class='ipsMenu_item'><a href='{url="app=calendar&module=calendar&controller=view" seoTemplate="calendar"}'><span class='cCalendarIcon cEvents_style_blank'></span> {lang="all_calendars"}</a></li>
{{foreach $calendars as $calendar}}
<li class='ipsMenu_item'><a href='{$calendar->url()}'><span class='cCalendarIcon cEvents_style{$calendar->id}'></span> {$calendar->_title}</a></li>
{{if $club = $calendar->club()}}
<li class='ipsMenu_item'><a href='{$calendar->url()}'><span class='cCalendarIcon cEvents_style_blank'></span> {lang="club_node" sprintf="$club->name, $calendar->_title"}</a></li>
{{else}}
<li class='ipsMenu_item'><a href='{$calendar->url()}'><span class='cCalendarIcon cEvents_style{$calendar->id}'></span> {$calendar->_title}</a></li>
{{endif}}
{{endforeach}}
</ul>
<ul class='ipsMenu ipsMenu_auto ipsHide' id='elCalendarSettings_menu'>
@@ -282,7 +288,7 @@
<li class='cEvents_ranged'>
<ul class='cEvents'>
{{foreach $events[ $date->year . '-' . $date->mon . '-' . $k ]['ranged'] as $event }}
<li class='cEvents_event cEvents_style{$event->calendar_id}{{if $event->nextOccurrence( \IPS\calendar\Date::getDate( $date->year, $date->mon, $k ), 'startDate' ) AND $event->nextOccurrence( \IPS\calendar\Date::getDate( $date->year, $date->mon, $k ), 'startDate' )->mysqlDatetime( FALSE ) == $date->year . '-' . $date->mon . '-' . $k }}{{$startDates[ $event->id ] = $date->year . '-' . $date->mon . '-' . $k;}} cEvents_first{{endif}}{{if $event->nextOccurrence( \IPS\calendar\Date::getDate( $date->year, $date->mon, $k ) ?: $event->nextOccurrence( \IPS\calendar\Date::getDate( $date->year, $date->mon, $k ), 'startDate' ), 'endDate' ) AND $event->nextOccurrence( \IPS\calendar\Date::getDate( $date->year, $date->mon, $k ) ?: $event->nextOccurrence( \IPS\calendar\Date::getDate( $date->year, $date->mon, $k ), 'startDate' ), 'endDate' )->mysqlDatetime( FALSE ) == $date->year . '-' . $date->mon . '-' . $k }} cEvents_last{{endif}}' data-eventID='{$event->id}'>
<li class='cEvents_event cEvents_style{$event->calendar_id}{{if $event->nextOccurrence( \IPS\calendar\Date::getDate( $date->year, $date->mon, $k ), 'startDate' ) AND $event->nextOccurrence( \IPS\calendar\Date::getDate( $date->year, $date->mon, $k ), 'startDate' )->mysqlDatetime( FALSE, TRUE ) == $date->year . '-' . $date->mon . '-' . $k }}{{$startDates[ $event->id ] = $date->year . '-' . $date->mon . '-' . $k;}} cEvents_first{{endif}}{{if $event->nextOccurrence( \IPS\calendar\Date::getDate( $date->year, $date->mon, $k ) ?: $event->nextOccurrence( \IPS\calendar\Date::getDate( $date->year, $date->mon, $k ), 'startDate' ), 'endDate' ) AND $event->nextOccurrence( \IPS\calendar\Date::getDate( $date->year, $date->mon, $k ) ?: $event->nextOccurrence( \IPS\calendar\Date::getDate( $date->year, $date->mon, $k ), 'startDate' ), 'endDate' )->mysqlDatetime( FALSE, TRUE ) == $date->year . '-' . $date->mon . '-' . $k }} cEvents_last{{endif}}' data-eventID='{$event->id}'>
<a href='{$event->url()}' data-ipsHover data-ipsHover-target='{$event->url('hovercard')->setQuerystring( 'sd', isset( $startDates[ $event->id ] ) ? $startDates[ $event->id ] : $date->year . '-' . $date->mon . '-' . $k )}' title='{$event->title}'>{{if !$event->all_day}}<span class='cEvents_time'>{$event->_start_date->localeTime( FALSE )}</span> {{endif}}{truncate="$event->title" length="15"}{{if $event->hidden() === 1}} <i class='fa fa-warning'></i>{{endif}}</a>
</li>
{{endforeach}}
@@ -425,7 +431,7 @@
<li class='cEvents_ranged'>
<ul class='cEvents'>
{{foreach $events[ $day->year . '-' . $day->mon . '-' . $day->mday ]['ranged'] as $event }}
<li class='cEvents_event cEvents_style{$event->calendar_id}{{if $event->nextOccurrence( $day, 'startDate' ) AND $event->nextOccurrence( $day, 'startDate' )->mysqlDatetime( FALSE ) == $day->mysqlDatetime( FALSE ) }}{{$startDates[ $event->id ] = $day->mysqlDatetime( FALSE );}} cEvents_first{{endif}}{{if $event->nextOccurrence( $day ?: $event->nextOccurrence( $day, 'startDate' ), 'endDate' ) AND $event->nextOccurrence( $day ?: $event->nextOccurrence( $day, 'startDate' ), 'endDate' )->mysqlDatetime( FALSE ) == $day->mysqlDatetime( FALSE ) }} cEvents_last{{endif}}'>
<li class='cEvents_event cEvents_style{$event->calendar_id}{{if $event->nextOccurrence( $day, 'startDate' ) AND $event->nextOccurrence( $day, 'startDate' )->mysqlDatetime( FALSE, TRUE ) == $day->mysqlDatetime( FALSE ) }}{{$startDates[ $event->id ] = $day->mysqlDatetime( FALSE );}} cEvents_first{{endif}}{{if $event->nextOccurrence( $day ?: $event->nextOccurrence( $day, 'startDate' ), 'endDate' ) AND $event->nextOccurrence( $day ?: $event->nextOccurrence( $day, 'startDate' ), 'endDate' )->mysqlDatetime( FALSE, TRUE ) == $day->mysqlDatetime( FALSE ) }} cEvents_last{{endif}}'>
<a href='{$event->url()}' data-ipsHover data-ipsHover-target='{$event->url('hovercard')->setQuerystring( 'sd', isset( $startDates[ $event->id ] ) ? $startDates[ $event->id ] : $day->mysqlDatetime( FALSE ) )}'>{$event->title}{{if $event->hidden() === 1}} <i class='fa fa-warning'></i>{{endif}}</a>
</li>
{{endforeach}}
@@ -459,7 +465,7 @@
<template template_group="browse" template_name="calendarWrapper" template_data="$calendar, $calendars, $thisCalendar, $jump, $date, $featured, $downloadLinks" template_location="front" template_app="calendar"><![CDATA[
{template="calendarHeader" group="browse" app="calendar" params="$calendars, $thisCalendar, $jump, $downloadLinks"}
<div data-controller='calendar.front.browse.main' class='ipsSpacer_top'>
<div data-controller='calendar.front.browse.main' class='ipsSpacer_top ipsBox_alt'>
{$calendar|raw}
</div>
@@ -486,7 +492,7 @@
{{foreach $collection as $input}}
{{if $input instanceof \IPS\Helpers\Form\Date}}
<li class='ipsFieldRow ipsFieldRow_fullWidth'>
<input type="date" data-control="date" placeholder="{lang="jump_to_placeholder"}" name='{$input->name}' id='{$input->htmlId}' {{if $input->required === TRUE}}required{{endif}} value="{{if $input->defaultValue instanceof \IPS\DateTime}}{$input->defaultValue->format('Y-m-d')}{{endif}}">
<input type="date" data-control="date" name='{$input->name}' id='{$input->htmlId}' {{if $input->required === TRUE}}required{{endif}} value="{{if $input->defaultValue instanceof \IPS\DateTime}}{$input->defaultValue->format('Y-m-d')}{{endif}}">
</li>
{{else}}
{$input->rowHtml($form)|raw}
@@ -525,35 +531,36 @@
</div>]]></template>
<template template_group="global" template_name="embedEvent" template_data="$item, $url, $image=NULL" template_location="front" template_app="calendar"><![CDATA[
<div data-embedInfo-maxSize='500' class='ipsRichEmbed'>
{template="embedHeader" group="embed" app="core" params="$item, \IPS\Member::loggedIn()->language()->addToStack( 'x_created_event_in', FALSE, array( 'sprintf' => array( $item->author()->name, $item->container()->_title ) ) ), $item->mapped('date')"}
{template="embedHeader" group="embed" app="core" params="$item, \IPS\Member::loggedIn()->language()->addToStack( 'x_created_event_in', FALSE, array( 'sprintf' => array( $item->author()->name, $item->container()->_title ) ) ), $item->mapped('date'), $url"}
{{if $item->map( 500, 100 )}}
<div class='ipsRichEmbed_masthead ipsType_center'>
<a href='{$item->url()}' title='{$item->mapped('title')}'>
<a href='{$url}' title='{$item->mapped('title')}'>
{$item->map( 500, 100 )|raw}
</a>
</div>
{{elseif $item->coverPhoto() && $item->coverPhoto()->file}}
{{$photo = $item->coverPhoto()->file;}}
<div class='ipsRichEmbed_masthead ipsRichEmbed_mastheadBg ipsType_center'>
<a href='{$item->url()}' title='{$item->mapped('title')}' style='background-image: url( "{expression="str_replace( array( '(', ')' ), array( '\(', '\)' ), $photo->url )"}") '>
<a href='{$url}' title='{$item->mapped('title')}' style='background-image: url( "{expression="str_replace( array( '(', ')' ), array( '\(', '\)' ), $photo->url )"}") '>
<img src='{$photo->url}' alt='{$item->mapped('title')}' class='ipsHide'>
</a>
</div>
{{endif}}
<div class='ipsPad ipsClearfix'>
<div class='ipsPad_double ipsClearfix'>
<div class='ipsPhotoPanel ipsPhotoPanel_medium'>
<span class='cCalendarEmbed_calendar ipsPos_left ipsType_blendLinks'>
<a href='{$item->url()}'>
<time datetime='{$item->nextOccurrence( \IPS\calendar\Date::getDate(), 'startDate' )->mysqlDatetime()}' class='ipsCalendarDate'>
<span class='ipsCalendarDate_month'>{$item->nextOccurrence( \IPS\calendar\Date::getDate(), 'startDate' )->monthNameShort}</span>
<span class='ipsCalendarDate_date'>{$item->nextOccurrence( \IPS\calendar\Date::getDate(), 'startDate' )->mday}</span>
<a href='{$url}'>
{{$nextOccurrence = $item->nextOccurrence( \IPS\calendar\Date::getDate(), 'startDate' ) ?: $event->lastOccurrence( 'startDate' );}}
<time datetime='{$nextOccurrence->mysqlDatetime()}' class='ipsCalendarDate'>
<span class='ipsCalendarDate_month'>{$nextOccurrence->monthNameShort}</span>
<span class='ipsCalendarDate_date'>{$nextOccurrence->mday}</span>
</time>
</a>
</span>
<div>
<p class='ipsRichEmbed_itemTitle ipsType_reset ipsTruncate ipsTruncate_line ipsType_blendLinks'>
<a href='{$item->url()}'>{$item->mapped('title')}</a>
<a href='{$url}'>{$item->mapped('title')}</a>
</p>
<div class='ipsType_reset ipsType_medium'>
{{if $item->_end_date}}
@@ -630,20 +637,20 @@
</div>]]></template>
<template template_group="global" template_name="embedEventComment" template_data="$comment, $item, $url" template_location="front" template_app="calendar"><![CDATA[
<div data-embedInfo-maxSize='500' class='ipsRichEmbed'>
{template="embedHeader" group="embed" app="core" params="$comment, \IPS\Member::loggedIn()->language()->addToStack( 'x_commented_event', FALSE, array( 'sprintf' => array( $comment->author()->name ) ) ), $comment->mapped('date')"}
<div class='ipsPad'>
{template="embedHeader" group="embed" app="core" params="$comment, \IPS\Member::loggedIn()->language()->addToStack( 'x_commented_event', FALSE, array( 'sprintf' => array( $comment->author()->name ) ) ), $comment->mapped('date'), $url"}
<div class='ipsPad_double'>
<div class='ipsRichEmbed_originalItem ipsAreaBackground_reset ipsSpacer_bottom ipsType_blendLinks'>
<div>
{{if $item->map( 500, 100 )}}
<div class='ipsRichEmbed_masthead ipsRichEmbed_masthead_small ipsType_center'>
<a href='{$item->url()}' title='{$item->mapped('title')}'>
<a href='{$url}' title='{$item->mapped('title')}'>
{$item->map( 500, 100 )|raw}
</a>
</div>
{{elseif $item->coverPhoto() && $item->coverPhoto()->file}}
{{$photo = $item->coverPhoto()->file;}}
<div class='ipsRichEmbed_masthead ipsRichEmbed_masthead_small ipsRichEmbed_mastheadBg ipsType_center'>
<a href='{$item->url()}' title='{$item->mapped('title')}' style='background-image: url( "{expression="str_replace( array( '(', ')' ), array( '\(', '\)' ), $photo->url )"}") '>
<a href='{$url}' title='{$item->mapped('title')}' style='background-image: url( "{expression="str_replace( array( '(', ')' ), array( '\(', '\)' ), $photo->url )"}") '>
<img src='{$photo->url}' alt='{$item->mapped('title')}' class='ipsHide'>
</a>
</div>
@@ -651,10 +658,11 @@
<div class='ipsPad'>
<div class='ipsPhotoPanel ipsPhotoPanel_small'>
<span class='cCalendarEmbed_calendar ipsPos_left ipsType_blendLinks'>
<a href='{$item->url()}'>
<time datetime='{$item->nextOccurrence( \IPS\calendar\Date::getDate(), 'startDate' )->mysqlDatetime()}' class='ipsCalendarDate'>
<span class='ipsCalendarDate_month'>{$item->nextOccurrence( \IPS\calendar\Date::getDate(), 'startDate' )->monthNameShort}</span>
<span class='ipsCalendarDate_date'>{$item->nextOccurrence( \IPS\calendar\Date::getDate(), 'startDate' )->mday}</span>
<a href='{$url}'>
{{$nextOccurrence = $item->nextOccurrence( \IPS\calendar\Date::getDate(), 'startDate' ) ?: $event->lastOccurrence( 'startDate' );}}
<time datetime='{$nextOccurrence->mysqlDatetime()}' class='ipsCalendarDate'>
<span class='ipsCalendarDate_month'>{$nextOccurrence->monthNameShort}</span>
<span class='ipsCalendarDate_date'>{$nextOccurrence->mday}</span>
</time>
</a>
</span>
@@ -670,7 +678,7 @@
{$comment->truncated(TRUE)|raw}
</div>
{{if \IPS\IPS::classUsesTrait( $comment, 'IPS\Content\Reactable' ) and count( $comment->reactions() ) and settings.reputation_enabled}}
{{if settings.reputation_enabled AND \IPS\IPS::classUsesTrait( $comment, 'IPS\Content\Reactable' ) AND count( $comment->reactions() )}}
<ul class='ipsList_inline ipsSpacer_top ipsSpacer_half'>
<li>
{template="reactionOverview" app="core" group="global" params="$comment, TRUE, 'small'"}
@@ -705,20 +713,20 @@
{{endif}}]]></template>
<template template_group="global" template_name="embedEventReview" template_data="$comment, $item, $url" template_location="front" template_app="calendar"><![CDATA[
<div data-embedInfo-maxSize='500' class='ipsRichEmbed'>
{template="embedHeader" group="embed" app="core" params="$comment, \IPS\Member::loggedIn()->language()->addToStack( 'x_reviewed_event', FALSE, array( 'sprintf' => array( $comment->author()->name ) ) ), $comment->mapped('date')"}
<div class='ipsPad'>
{template="embedHeader" group="embed" app="core" params="$comment, \IPS\Member::loggedIn()->language()->addToStack( 'x_reviewed_event', FALSE, array( 'sprintf' => array( $comment->author()->name ) ) ), $comment->mapped('date'), $url"}
<div class='ipsPad_double'>
<div class='ipsRichEmbed_originalItem ipsAreaBackground_reset ipsSpacer_bottom ipsType_blendLinks'>
<div>
{{if $item->map( 500, 100 )}}
<div class='ipsRichEmbed_masthead ipsRichEmbed_masthead_small ipsType_center'>
<a href='{$item->url()}' title='{$item->mapped('title')}'>
<a href='{$url}' title='{$item->mapped('title')}'>
{$item->map( 500, 100 )|raw}
</a>
</div>
{{elseif $item->coverPhoto() && $item->coverPhoto()->file}}
{{$photo = $item->coverPhoto()->file;}}
<div class='ipsRichEmbed_masthead ipsRichEmbed_masthead_small ipsRichEmbed_mastheadBg ipsType_center'>
<a href='{$item->url()}' title='{$item->mapped('title')}' style='background-image: url( "{expression="str_replace( array( '(', ')' ), array( '\(', '\)' ), $photo->url )"}") '>
<a href='{$url}' title='{$item->mapped('title')}' style='background-image: url( "{expression="str_replace( array( '(', ')' ), array( '\(', '\)' ), $photo->url )"}") '>
<img src='{$photo->url}' alt='{$item->mapped('title')}' class='ipsHide'>
</a>
</div>
@@ -726,10 +734,11 @@
<div class='ipsPad'>
<div class='ipsPhotoPanel ipsPhotoPanel_small'>
<span class='cCalendarEmbed_calendar ipsPos_left ipsType_blendLinks'>
<a href='{$item->url()}'>
<time datetime='{$item->nextOccurrence( \IPS\calendar\Date::getDate(), 'startDate' )->mysqlDatetime()}' class='ipsCalendarDate'>
<span class='ipsCalendarDate_month'>{$item->nextOccurrence( \IPS\calendar\Date::getDate(), 'startDate' )->monthNameShort}</span>
<span class='ipsCalendarDate_date'>{$item->nextOccurrence( \IPS\calendar\Date::getDate(), 'startDate' )->mday}</span>
<a href='{$url}'>
{{$nextOccurrence = $item->nextOccurrence( \IPS\calendar\Date::getDate(), 'startDate' ) ?: $event->lastOccurrence( 'startDate' );}}
<time datetime='{$nextOccurrence->mysqlDatetime()}' class='ipsCalendarDate'>
<span class='ipsCalendarDate_month'>{$nextOccurrence->monthNameShort}</span>
<span class='ipsCalendarDate_date'>{$nextOccurrence->mday}</span>
</time>
</a>
</span>
@@ -750,7 +759,7 @@
{$comment->truncated(TRUE)|raw}
</div>
{{if \IPS\IPS::classUsesTrait( $comment, 'IPS\Content\Reactable' ) and count( $comment->reactions() ) and settings.reputation_enabled}}
{{if settings.reputation_enabled AND \IPS\IPS::classUsesTrait( $comment, 'IPS\Content\Reactable' ) AND count( $comment->reactions() )}}
<ul class='ipsList_inline ipsSpacer_top ipsSpacer_half'>
<li>
{template="reactionOverview" app="core" group="global" params="$comment, TRUE, 'small'"}
@@ -869,7 +878,7 @@
{{endif}}
</div>
<div class='ipsDataItem_main'>
<h4 class='ipsDataItem_title ipsType_sectionHead ipsType_break'>
<h4 class='ipsDataItem_title ipsType_sectionHead ipsContained_container'>
{{if $row->mapped('locked')}}
<i class="fa fa-lock"></i>
{{endif}}
@@ -889,7 +898,7 @@
{{endif}}
{{if $row->prefix()}}
{template="prefix" group="global" app="core" params="$row->prefix( TRUE ), $row->prefix()"}
<span>{template="prefix" group="global" app="core" params="$row->prefix( TRUE ), $row->prefix()"}</span>
{{endif}}
<span class='ipsType_break ipsContained'>
@@ -924,11 +933,8 @@
{{endif}}
{{endif}}
{{if count( $row->tags() )}}
<ul class='ipsTags ipsList_inline'>
{{foreach $row->tags() as $tag}}
<li><a href="{url="app=core&module=search&controller=search&tags={$tag}" seoTemplate="tags"}">{$tag}</a></li>
{{endforeach}}
</ul>
&nbsp;&nbsp;
{template="tags" group="global" app="core" params="$row->tags(), true, true"}
{{endif}}
</div>
<div class='ipsDataItem_generic ipsDataItem_size8'>
@@ -993,7 +999,7 @@
{{endif}}
{{if trim( $indexData['index_content'] ) !== ''}}
<div class='ipsType_richText ipsType_break ipsType_medium ipsSpacer_top ipsSpacer_half' {{if !( \IPS\Dispatcher::i()->application->directory == 'core' and \IPS\Dispatcher::i()->module and \IPS\Dispatcher::i()->module->key == 'search' )}}data-ipsTruncate data-ipsTruncate-type='remove' data-ipsTruncate-size='3 lines' data-ipsTruncate-watch='false'{{else}}data-searchable data-findTerm{{endif}}>
{$indexData['index_content']|raw}
{expression="\IPS\Content\Search\Result::preDisplay( $indexData['index_content'] )" raw="true"}
</div>
{{endif}}
</div>
@@ -1029,7 +1035,7 @@
{{if trim( $indexData['index_content'] ) !== ''}}
<div class='ipsSpacer_top ipsSpacer_half ipsType_richText ipsType_break ipsType_medium' {{if !( \IPS\Dispatcher::i()->application->directory == 'core' and \IPS\Dispatcher::i()->module and \IPS\Dispatcher::i()->module->key == 'search' )}}data-ipsTruncate data-ipsTruncate-type='remove' data-ipsTruncate-size='2 lines' data-ipsTruncate-watch='false'{{else}}data-searchable data-findTerm{{endif}}>
{$indexData['index_content']|raw}
{expression="\IPS\Content\Search\Result::preDisplay( $indexData['index_content'] )" raw="true"}
</div>
{{endif}}
</div>
@@ -1154,6 +1160,15 @@
<i class='fa fa-clock-o'></i>
<input name="event_dates[end_time]" id="end_time" type="time" size='12' class="ipsField_short" placeholder="{lang="_time_format_hhmm"}" step='60' min='00:00' value="{$value['end_time']}">
</div>
<br><br>
<ul class='ipsList_inline' id='end_date_controls'>
<li>
<span class='ipsCustomInput'>
<input type="checkbox" role="checkbox" name="event_dates[no_end_time]" {{if !$value['end_time']}}checked aria-checked='true'{{endif}} value="1" id="check_no_end_time">
<span></span>
</span> <label for='check_no_end_time'>{lang="no_end_time"}</label>
</li>
</ul>
</div>
</div>
</div>
@@ -1269,14 +1284,14 @@
</div>
</div>]]></template>
<template template_group="submit" template_name="submitForm" template_data="$id, $action, $elements, $hiddenValues, $actionButtons, $uploadField, $class=''" template_location="front" template_app="calendar"><![CDATA[
<form accept-charset='utf-8' class="ipsForm {$class}" action="{$action}" method="post" {{if $uploadField}}enctype="multipart/form-data"{{endif}} data-ipsForm>
<form accept-charset='utf-8' class="ipsForm {$class}" action="{$action}" method="post" {{if $uploadField}}enctype="multipart/form-data"{{endif}} data-ipsForm data-ipsFormSubmit>
<input type="hidden" name="{$id}_submitted" value="1">
{{foreach $hiddenValues as $k => $v}}
<input type="hidden" name="{$k}" value="{$v}">
{{endforeach}}
{{if $uploadField}}
<input type="hidden" name="MAX_FILE_SIZE" value="{$uploadField}">
<input type="hidden" name="plupload" value="{expression="md5( uniqid() )"}">
<input type="hidden" name="plupload" value="{expression="md5( mt_rand() )"}">
{{endif}}
<div class='ipsBox'>
@@ -1439,14 +1454,16 @@
</ul>
{{endif}}
</div>]]></template>
<template template_group="view" template_name="comments" template_data="$event" template_location="front" template_app="calendar"><![CDATA[<div class='' data-controller='core.front.core.commentFeed, core.front.core.ignoredComments' {{if settings.auto_polling_enabled}}data-autoPoll{{endif}} data-baseURL='{$event->url()}' {{if $event->isLastPage()}}data-lastPage{{endif}} data-feedID='event-{$event->id}' id='comments'>
<template template_group="view" template_name="comments" template_data="$event" template_location="front" template_app="calendar"><![CDATA[
<div class='' data-controller='core.front.core.commentFeed, core.front.core.ignoredComments' {{if settings.auto_polling_enabled}}data-autoPoll{{endif}} data-baseURL='{$event->url()}' {{if $event->isLastPage()}}data-lastPage{{endif}} data-feedID='event-{$event->id}' id='comments'>
{template="featuredComments" group="global" app="core" params="$event->featuredComments(), $event->url()->setQueryString('tab', 'comments')->setQueryString('recommended', 'comments')"}
{template="commentMultimodHeader" app="core" group="global" params="$event, '#comments'"}
{{if $event->commentPageCount() > 1}}
{$event->commentPagination( array( 'tab' ) )|raw}
<br><br>
{{endif}}
{template="featuredComments" group="global" app="core" params="$event->featuredComments(), $event->url()->setQueryString('tab', 'comments')->setQueryString('recommended', 'comments')"}
<div data-role='commentFeed' data-controller='core.front.core.moderation'>
{{if ( $comments = $event->comments( NULL, NULL, 'date', 'asc', NULL, NULL, NULL, NULL, FALSE, isset( \IPS\Request::i()->showDeleted ) ) and count( $comments ) )}}
<form action="{$event->url()->csrf()->setQueryString( 'do', 'multimodComment' )}" method="post" data-ipsPageAction data-role='moderationTools'>
@@ -1470,7 +1487,7 @@
<hr class='ipsHr'>
{$event->commentPagination( array( 'tab' ) )|raw}
{{endif}}
{{if $event->commentForm() || $event->locked() || \IPS\Member::loggedIn()->restrict_post || \IPS\Member::loggedIn()->members_bitoptions['unacknowledged_warnings']}}
{{if $event->commentForm() || $event->locked() || \IPS\Member::loggedIn()->restrict_post || \IPS\Member::loggedIn()->members_bitoptions['unacknowledged_warnings'] || !\IPS\Member::loggedIn()->checkPostsPerDay()}}
<a id='replyForm'></a>
<div data-role='replyArea' class='ipsBox ipsBox_transparent ipsAreaBackground ipsPad {{if !$event->canComment()}}cTopicPostArea_noSize{{endif}} ipsSpacer_top'>
{{if $event->commentForm()}}
@@ -1485,6 +1502,8 @@
{template="commentUnavailable" group="forms" location="front" app="core" params="'restricted_cannot_comment', \IPS\Member::loggedIn()->warnings(5,NULL,'rpa'), \IPS\Member::loggedIn()->restrict_post"}
{{elseif \IPS\Member::loggedIn()->members_bitoptions['unacknowledged_warnings']}}
{template="commentUnavailable" group="forms" location="front" app="core" params="'unacknowledged_warning_cannot_post', \IPS\Member::loggedIn()->warnings( 1, FALSE )"}
{{elseif !\IPS\Member::loggedIn()->checkPostsPerDay()}}
{template="commentUnavailable" group="forms" location="front" app="core" params="'member_exceeded_posts_per_day'"}
{{endif}}
{{endif}}
</div>
@@ -1548,7 +1567,7 @@
</div>]]></template>
<template template_group="view" template_name="eventBlock" template_data="$event, $date, $truncate=FALSE, $map=array( 300, 200 ), $revertToFirst=FALSE" template_location="front" template_app="calendar"><![CDATA[{{$coverPhoto = $event->coverPhoto();}}
<div class='cCalendarBlock ipsAreaBackground_reset ipsBox'>
<div class='ipsPageHead_special ipsCoverPhoto' data-controller='core.front.core.coverPhoto' data-coverOffset='{$coverPhoto->offset}'>
<div class='ipsPageHead_special ipsCoverPhoto' data-controller='core.global.core.coverPhoto' data-coverOffset='{$coverPhoto->offset}'>
{{if $coverPhoto->file}}
<div class='ipsCoverPhoto_container'>
<img src='{$coverPhoto->file->url}' class='ipsCoverPhoto_photo'>
@@ -1643,6 +1662,14 @@
{$event->map( 250, 150 )|raw}
</div>
{{endif}}
{{if $event->venue}}
<div class='ipsSpacer_bottom'>
<h3 class='ipsType_minorHeading'>
<strong>{lang="event_venue_name"}</strong>
</h3>
{lang="calendar_venue_{$event->venue()->id}" escape="true"}
</div>
{{endif}}
{{if $address}}
<h3 class='ipsType_minorHeading'>
<strong>{lang="event_address"}</strong>
@@ -1678,10 +1705,14 @@
<ul class='ipsGrid ipsList_reset'>
{{foreach $attendees[1] as $attendee}}
<li class='ipsGrid_span3'>
{{if \IPS\Member::loggedIn()->canAccessModule( \IPS\Application\Module::get( 'core', 'members' ) ) }}
<a href='{$attendee->url()}' title="{$attendee->name}" data-ipsTooltip>
{{endif}}
<img src='{$attendee->photo}' class='ipsUserPhoto ipsUserPhoto_mini' alt=''>
<span class='ipsHide'>{$attendee->name}</span>
{{if \IPS\Member::loggedIn()->canAccessModule( \IPS\Application\Module::get( 'core', 'members' ) ) }}
</a>
{{endif}}
</li>
{{endforeach}}
</ul>
@@ -1697,10 +1728,14 @@
<ul class='ipsGrid ipsList_reset'>
{{foreach $attendees[2] as $attendee}}
<li class='ipsGrid_span3'>
{{if \IPS\Member::loggedIn()->canAccessModule( \IPS\Application\Module::get( 'core', 'members' ) ) }}
<a href='{$attendee->url()}' title="{$attendee->name}" data-ipsTooltip>
{{endif}}
<img src='{$attendee->photo}' class='ipsUserPhoto ipsUserPhoto_mini' alt=''>
<span class='ipsHide'>{$attendee->name}</span>
{{if \IPS\Member::loggedIn()->canAccessModule( \IPS\Application\Module::get( 'core', 'members' ) ) }}
</a>
{{endif}}
</li>
{{endforeach}}
</ul>
@@ -1714,10 +1749,14 @@
<ul class='ipsGrid ipsList_reset'>
{{foreach $attendees[0] as $attendee}}
<li class='ipsGrid_span3'>
{{if \IPS\Member::loggedIn()->canAccessModule( \IPS\Application\Module::get( 'core', 'members' ) ) }}
<a href='{$attendee->url()}' title="{$attendee->name}" data-ipsTooltip>
{{endif}}
<img src='{$attendee->photo}' class='ipsUserPhoto ipsUserPhoto_mini' alt=''>
<span class='ipsHide'>{$attendee->name}</span>
{{if \IPS\Member::loggedIn()->canAccessModule( \IPS\Application\Module::get( 'core', 'members' ) ) }}
</a>
{{endif}}
</li>
{{endforeach}}
</ul>
@@ -1736,7 +1775,7 @@
{{endif}}]]></template>
<template template_group="view" template_name="eventStreamBlock" template_data="$event, $date, $truncate=FALSE, $map=array( 300, 200 ), $revertToFirst=FALSE" template_location="front" template_app="calendar"><![CDATA[{{$coverPhoto = $event->coverPhoto();}}
<div class='cCalendarBlock ipsAreaBackground_reset ipsBox ipsPad'>
<div class='ipsPageHead_special ipsCoverPhoto' data-controller='core.front.core.coverPhoto' data-coverOffset='{$coverPhoto->offset}'>
<div class='ipsPageHead_special ipsCoverPhoto' data-controller='core.global.core.coverPhoto' data-coverOffset='{$coverPhoto->offset}'>
{{if $coverPhoto->file}}
<div class='ipsCoverPhoto_container'>
<img src='{$coverPhoto->file->url}' class='ipsCoverPhoto_photo'>
@@ -1759,9 +1798,15 @@
<br>
<div class='eventStreamDate ipsPos_left'>
<a href='{$event->url()}'>
<time datetime='{$event->nextOccurrence( \IPS\calendar\Date::getDate(), 'startDate' )->mysqlDatetime()}' class='ipsCalendarDate'>
<span class='ipsCalendarDate_month'>{$event->nextOccurrence( \IPS\calendar\Date::getDate(), 'startDate' )->monthNameShort}</span>
<span class='ipsCalendarDate_date'>{$event->nextOccurrence( \IPS\calendar\Date::getDate(), 'startDate' )->mday}</span>
{{if $event->nextOccurrence( \IPS\calendar\Date::getDate(), 'startDate' )}}
<time datetime='{$event->nextOccurrence( \IPS\calendar\Date::getDate(), 'startDate' )->mysqlDatetime()}' class='ipsCalendarDate'>
<span class='ipsCalendarDate_month'>{$event->nextOccurrence( \IPS\calendar\Date::getDate(), 'startDate' )->monthNameShort}</span>
<span class='ipsCalendarDate_date'>{$event->nextOccurrence( \IPS\calendar\Date::getDate(), 'startDate' )->mday}</span>
{{else}}
<time datetime='{$event->lastOccurrence( 'startDate' )->mysqlDatetime()}{{if !$event->all_day}} {$event->lastOccurrence( 'startDate' )->mysqlDatetime( FALSE )} {{endif}}' class='ipsCalendarDate'>
<span class='ipsCalendarDate_month'>{$event->lastOccurrence( 'startDate' )->monthNameShort}</span>
<span class='ipsCalendarDate_date'>{$event->lastOccurrence( 'startDate' )->mday}</span>
{{endif}}
</time>
</a>
</div>
@@ -1830,8 +1875,8 @@
<input type="hidden" name="{$k}" value="{$v}">
{{endforeach}}
{{if $uploadField}}
<input type="hidden" name="MAX_FILE_SIZE" value="{$uploadField}">
<input type="hidden" name="plupload" value="{expression="md5( uniqid() )"}">
<input type="hidden" name="MAX_FILE_SIZE" value="{$uploadField}">
<input type="hidden" name="plupload" value="{expression="md5( mt_rand() )"}">
{{endif}}
<div class="ipsPad">
<h2 class='ipsType_sectionHead'>{expression="\IPS\Output::i()->title"}</h2>
@@ -1855,7 +1900,8 @@
{$actionButtons[0]|raw} {{if isset( $actionButtons[1] )}}{$actionButtons[1]|raw}{{endif}}
</div>
</form>]]></template>
<template template_group="view" template_name="reviews" template_data="$event" template_location="front" template_app="calendar"><![CDATA[<div data-controller='core.front.core.commentFeed' {{if settings.auto_polling_enabled}}data-autoPoll{{endif}} data-commentsType='reviews' data-baseURL='{$event->url()}' {{if $event->isLastPage('reviews')}}data-lastPage{{endif}} data-feedID='event-{$event->id}-reviews' id='reviews'>
<template template_group="view" template_name="reviews" template_data="$event" template_location="front" template_app="calendar"><![CDATA[
<div data-controller='core.front.core.commentFeed' {{if settings.auto_polling_enabled}}data-autoPoll{{endif}} data-commentsType='reviews' data-baseURL='{$event->url()}' {{if $event->isLastPage('reviews')}}data-lastPage{{endif}} data-feedID='event-{$event->id}-reviews' id='reviews'>
{{if $event->reviewForm()}}
{{if $event->locked()}}
<strong class='ipsType_warning'><i class='fa fa-info-circle'></i> {lang="item_locked_can_review"}</strong>
@@ -1881,6 +1927,7 @@
{{if count( $event->reviews( NULL, NULL, NULL, 'desc', NULL, NULL, NULL, NULL, isset( \IPS\Request::i()->showDeleted ) ) )}}
{{if !$event->hasReviewed()}}<hr class='ipsHr'>{{endif}}
<div class="ipsClearfix ipsSpacer_bottom">
{template="commentMultimodHeader" app="core" group="global" params="$event, '#reviews', 'review'"}
{{if $event->reviewPageCount() > 1}}
<div class="ipsPos_left ipsResponsive_noFloat">
{$event->reviewPagination( array( 'tab', 'sort' ) )|raw}
@@ -2120,14 +2167,14 @@
</li>
{{endif}}
{{if $event->canReport() }}
{{if $event->canReportOrRevoke() === TRUE}}
<li>
<a href='{$event->url('report')}' data-ipsDialog data-ipsDialog-size='medium' data-ipsDialog-title="{lang="report_event"}" data-ipsDialog-remoteSubmit data-ipsDialog-flashMessage="{lang="report_submit_success"}" title="{lang="report_event"}" class='ipsButton ipsButton_link ipsButton_verySmall ipsButton_fullWidth'>{lang="report_event"}</a>
</li>
{{endif}}
<li><a href='{$event->url('download')}' rel="noindex, nofollow" title='{lang="download_ical_title"}' class='ipsButton ipsButton_link ipsButton_verySmall ipsButton_fullWidth'>{lang="download_ical"}</a></li>
{{if $event->venue() and $event->venue()->can( 'view' )}}
<li><a href="{$event->venue()->url()}" class='ipsButton ipsButton_link ipsButton_verySmall ipsButton_fullWidth'>{lang="calendar_more_events_at_x" sprintf=" $event->venue()->_title"}</a></li>
<li><a href="{$event->venue()->url()}" class='ipsButton ipsButton_link ipsButton_verySmall ipsButton_fullWidth'>{lang="calendar_more_events_at_x" sprintf="$event->venue()->_title"}</a></li>
{{endif}}
{{if $event->author() == \IPS\Member::loggedIn() and \IPS\calendar\Calendar::canOnAny('add')}}
<li><a href='{url="app=calendar&module=calendar&controller=submit&do=copy&event_id={$event->id}"}' rel="noindex, nofollow" title='' class='ipsButton ipsButton_link ipsButton_verySmall ipsButton_fullWidth'>{lang="add_similar_event"}</a></li>
@@ -2265,70 +2312,87 @@
]]></template>
<template template_group="widgets" template_name="upcomingEvents" template_data="$events, $today, $orientation='vertical'" template_location="front" template_app="calendar"><![CDATA[<h3 class='ipsType_reset ipsWidget_title'>{lang="block_upcomingEvents"}</h3>
<div class='ipsWidget_inner'>
{{if $orientation == 'vertical'}}
{{if $orientation == 'vertical'}}
<div class='ipsPad_half'>
{{endif}}
{{if !empty( $events ) }}
{{endif}}
{{if !empty( $events ) }}
<ul class='ipsDataList ipsDataList_reducedSpacing'>
{{foreach $events as $event}}
{{foreach $events as $event}}
<li class='ipsDataItem ipsClearfix'>
<div class='ipsDataItem_icon cCalendar_date_overlay'>
{{if $event->nextOccurrence( $today, 'startDate' )}}
<time datetime='{$event->nextOccurrence( $today, 'startDate' )->mysqlDatetime()}' class='ipsCalendarDate'>
<span class='ipsCalendarDate_month' data-controller="core.global.core.datetime" data-time="{$event->nextOccurrence( $today, 'startDate' )->format('c')}" data-format="%b">{$event->nextOccurrence( $today, 'startDate' )->monthNameShort}</span>
<span class='ipsCalendarDate_date' data-controller="core.global.core.datetime" data-time="{$event->nextOccurrence( $today, 'startDate' )->format('c')}" data-format="%d">{$event->nextOccurrence( $today, 'startDate' )->mday}</span>
</time>
<time datetime='{$event->nextOccurrence( $today, 'startDate' )->mysqlDatetime()}' class='ipsCalendarDate'>
<span class='ipsCalendarDate_month' data-controller="core.global.core.datetime" data-time="{$event->nextOccurrence( $today, 'startDate' )->format('c')}" data-format="%b">{$event->nextOccurrence( $today, 'startDate' )->monthNameShort}</span>
<span class='ipsCalendarDate_date' data-controller="core.global.core.datetime" data-time="{$event->nextOccurrence( $today, 'startDate' )->format('c')}" data-format="%d">{$event->nextOccurrence( $today, 'startDate' )->mday}</span>
</time>
{{else}}
<time datetime='{$event->lastOccurrence( 'startDate' )->mysqlDatetime()}' class='ipsCalendarDate'>
<span class='ipsCalendarDate_month' data-controller="core.global.core.datetime" data-time="{$event->lastOccurrence( $today, 'startDate' )->format('c')}" data-format="%b">{$event->lastOccurrence( 'startDate' )->monthNameShort}</span>
<span class='ipsCalendarDate_date' data-controller="core.global.core.datetime" data-time="{$event->lastOccurrence( $today, 'startDate' )->format('c')}" data-format="%d">{$event->lastOccurrence( 'startDate' )->mday}</span>
</time>
<time datetime='{$event->lastOccurrence( 'startDate' )->mysqlDatetime()}' class='ipsCalendarDate'>
<span class='ipsCalendarDate_month' data-controller="core.global.core.datetime" data-time="{$event->lastOccurrence( 'startDate' )->format('c')}" data-format="%b">{$event->lastOccurrence( 'startDate' )->monthNameShort}</span>
<span class='ipsCalendarDate_date' data-controller="core.global.core.datetime" data-time="{$event->lastOccurrence( 'startDate' )->format('c')}" data-format="%d">{$event->lastOccurrence( 'startDate' )->mday}</span>
</time>
{{endif}}
</div>
<div class='ipsDataItem_main cWidgetComments'>
{{if $event->container()->allow_comments && $orientation == 'vertical'}}
<div class="ipsCommentCount ipsPos_right {{if $event->comments === 0}}ipsFaded{{endif}}" data-ipsTooltip title='{lang="num_replies" pluralize="$event->comments"}'>{expression="$event->comments"}</div>
<div class="ipsCommentCount ipsPos_right {{if $event->comments === 0}}ipsFaded{{endif}}" data-ipsTooltip title='{lang="num_replies" pluralize="$event->comments"}'>{expression="$event->comments"}</div>
{{endif}}
<div class='ipsType_break ipsContained'>
<a href="{$event->url()}" title='{lang="view_this_event" sprintf="$event->title"}'>{$event->title}</a>
</div>
<strong class='ipsType_small'>
{{$sameDay = (bool) ( $event->nextOccurrence( $event->nextOccurrence( $today, 'startDate' ) ?: $today, 'endDate' ) and $event->nextOccurrence( $today, 'startDate' )->calendarDate() == $event->nextOccurrence( $event->nextOccurrence( $today, 'startDate' ) ?: $today, 'endDate' )->calendarDate() );}}
{{if $event->nextOccurrence( $today, 'startDate' )}}
<span data-controller="core.global.core.datetime" data-time="{$event->nextOccurrence( $today, 'startDate' )->format('c')}" data-format="{expression="\IPS\calendar\Date::calendarDateFormat()"}{{if !$event->all_day}} {expression="\IPS\calendar\Date::localeTimeFormat( FALSE )"} {{endif}}">{$event->nextOccurrence( $today, 'startDate' )->calendarDate()}{{if !$event->all_day}} {$event->nextOccurrence( $today, 'startDate' )->localeTime( FALSE )} {{endif}}</span>
{{if $event->nextOccurrence( $event->nextOccurrence( $today, 'startDate' ) ?: $today, 'endDate' ) }}
{{if $orientation == 'vertical'}}<br>{{endif}}
<span class='ipsType_light ipsType_unbold'>{lang="until"}</span>{{if $orientation == 'vertical' and !$sameDay}}<br>{{endif}}
{{if !$sameDay}}<span data-controller="core.global.core.datetime" data-time="{$event->nextOccurrence( $event->nextOccurrence( $today, 'startDate' ) ?: $today, 'endDate' )->format('c')}" data-format="{expression="\IPS\calendar\Date::calendarDateFormat()"}{{if !$event->all_day}} {expression="\IPS\calendar\Date::localeTimeFormat( FALSE )"}{{endif}}">{$event->nextOccurrence( $event->nextOccurrence( $today, 'startDate' ) ?: $today, 'endDate' )->calendarDate()}{{endif}}{{if !$event->all_day}} {$event->nextOccurrence( $event->nextOccurrence( $today, 'startDate' ) ?: $today, 'endDate' )->localeTime( FALSE )}{{endif}}</span>
{{endif}}
<span data-controller="core.global.core.datetime" data-time="{$event->nextOccurrence( $today, 'startDate' )->format('c')}" data-format="{expression="\IPS\calendar\Date::calendarDateFormat()"}{{if !$event->all_day}} {expression="\IPS\calendar\Date::localeTimeFormat( FALSE )"} {{endif}}">{$event->nextOccurrence( $today, 'startDate' )->calendarDate()}{{if !$event->all_day}} {$event->nextOccurrence( $today, 'startDate' )->localeTime( FALSE )} {{endif}}</span>
{{if $event->nextOccurrence( $event->nextOccurrence( $today, 'startDate' ) ?: $today, 'endDate' ) }}
{{$sameDay = (bool) ( $event->nextOccurrence( $event->nextOccurrence( $today, 'startDate' ) ?: $today, 'endDate' ) and ( $event->nextOccurrence( $today, 'startDate' ) and $event->nextOccurrence( $today, 'startDate' )->calendarDate() == $event->nextOccurrence( $event->nextOccurrence( $today, 'startDate' ) ?: $today, 'endDate' )->calendarDate() ) );}}
{{if $orientation == 'vertical'}}<br>{{endif}}
<span class='ipsType_light ipsType_unbold'>{lang="until"}</span>{{if $orientation == 'vertical' and !$sameDay}}<br>{{endif}}
{{if !$sameDay}}
<span data-controller="core.global.core.datetime" data-time="{$event->nextOccurrence( $event->nextOccurrence( $today, 'startDate' ) ?: $today, 'endDate' )->format('c')}" data-format="{expression="\IPS\calendar\Date::calendarDateFormat()"}{{if !$event->all_day}} {expression="\IPS\calendar\Date::localeTimeFormat( FALSE )"}{{endif}}">
{$event->nextOccurrence( $event->nextOccurrence( $today, 'startDate' ) ?: $today, 'endDate' )->calendarDate()}
{{if !$event->all_day}} {$event->nextOccurrence( $event->nextOccurrence( $today, 'startDate' ) ?: $today, 'endDate' )->localeTime( FALSE )}{{endif}}
</span>
{{else}}
{{if !$event->all_day}}
<span data-controller="core.global.core.datetime" data-time="{$event->nextOccurrence( $event->nextOccurrence( $today, 'startDate' ) ?: $today, 'endDate' )->format('c')}" data-format="{expression="\IPS\calendar\Date::localeTimeFormat( FALSE )"}">
{$event->nextOccurrence( $event->nextOccurrence( $today, 'startDate' ) ?: $today, 'endDate' )->localeTime( FALSE )}
</span>
{{endif}}
{{endif}}
{{endif}}
{{else}}
<span data-controller="core.global.core.datetime" data-time="{$event->lastOccurrence( 'startDate' )->format('c')}" data-format="{expression="\IPS\calendar\Date::calendarDateFormat()"}{{if !$event->all_day}} {expression="\IPS\calendar\Date::localeTimeFormat( FALSE )"} {{endif}}">{$event->lastOccurrence( 'startDate' )->calendarDate()}{{if !$event->all_day}} {$event->lastOccurrence( 'startDate' )->localeTime( FALSE )} </span>{{endif}}
{{if $event->lastOccurrence( 'endDate' ) }}
{{if $orientation == 'vertical'}}<br>{{endif}}
<span class='ipsType_light ipsType_unbold'>{lang="until"}</span>{{if $orientation == 'vertical'}}<br>{{endif}}
<span data-controller="core.global.core.datetime" data-time="{$event->lastOccurrence( 'endDate' )->format('c')}" data-format="{expression="\IPS\calendar\Date::calendarDateFormat()"}{{if !$event->all_day}} {expression="\IPS\calendar\Date::localeTimeFormat( FALSE )"}{{endif}}">{$event->lastOccurrence( 'endDate' )->calendarDate()}{{if !$event->all_day}} {$event->lastOccurrence( 'endDate' )->localeTime( FALSE )}{{endif}}</span>
{{endif}}
<span data-controller="core.global.core.datetime" data-time="{$event->lastOccurrence( 'startDate' )->format('c')}" data-format="{expression="\IPS\calendar\Date::calendarDateFormat()"}{{if !$event->all_day}} {expression="\IPS\calendar\Date::localeTimeFormat( FALSE )"} {{endif}}">{$event->lastOccurrence( 'startDate' )->calendarDate()}{{if !$event->all_day}} {$event->lastOccurrence( 'startDate' )->localeTime( FALSE )} </span>{{endif}}
{{if $event->lastOccurrence( 'endDate' ) }}
{{if $orientation == 'vertical'}}<br>{{endif}}
<span class='ipsType_light ipsType_unbold'>{lang="until"}</span>{{if $orientation == 'vertical'}}<br>{{endif}}
<span data-controller="core.global.core.datetime" data-time="{$event->lastOccurrence( 'endDate' )->format('c')}" data-format="{expression="\IPS\calendar\Date::calendarDateFormat()"}{{if !$event->all_day}} {expression="\IPS\calendar\Date::localeTimeFormat( FALSE )"}{{endif}}">{$event->lastOccurrence( 'endDate' )->calendarDate()}{{if !$event->all_day}} {$event->lastOccurrence( 'endDate' )->localeTime( FALSE )}{{endif}}</span>
{{endif}}
{{endif}}
</strong>
<br>
{{if $event->container()->allow_comments && $orientation == 'horizontal'}}
<span class="{{if $event->comments === 0}}ipsFaded{{endif}}" data-ipsTooltip title='{lang="num_replies" pluralize="$event->comments"}'><i class='fa fa-comment'></i> {expression="$event->comments"}</span>&nbsp;&nbsp;
<span class="{{if $event->comments === 0}}ipsFaded{{endif}}" data-ipsTooltip title='{lang="num_replies" pluralize="$event->comments"}'><i class='fa fa-comment'></i> {expression="$event->comments"}</span>&nbsp;&nbsp;
{{endif}}
{{if $orientation == 'horizontal'}}
<div class='ipsType_medium ipsType_richText' data-ipsTruncate data-ipsTruncate-type='remove' data-ipsTruncate-size='2 lines' data-ipsTruncate-watch='false'>
{$event->truncated()|raw}
</div>
<div class='ipsType_medium ipsType_richText' data-ipsTruncate data-ipsTruncate-type='remove' data-ipsTruncate-size='2 lines' data-ipsTruncate-watch='false'>
{$event->truncated()|raw}
</div>
{{endif}}
</div>
</li>
{{endforeach}}
{{endforeach}}
</ul>
{{else}}
{{else}}
<div class='ipsType_light ipsPad_half ipsType_center'>{lang="no_upcoming_events"}</div>
{{endif}}
{{if $orientation == 'vertical'}}
</div>
{{endif}}
{{if $orientation == 'vertical'}}
</div>
{{endif}}
</div>]]></template>
<css css_location="front" css_app="calendar" css_attributes="" css_path="." css_name="calendar_responsive.css"><![CDATA[/* BOTH */
@media screen and (max-width: 979px) {
@@ -2774,6 +2838,11 @@ tr td:not( :first-child ):not( .cCalendar_firstDay ) .cEvents_ranged .cEvents_ev
margin-top: 5px;
}
#elEventHeader > .ipsCoverPhotoMinimal {
padding-top: 25px;
height: 100px;
}
.cEvent_to {
font-size: 28px;
margin: 6px 15px;
+52 -1
View File
@@ -170,5 +170,56 @@
"101117": "4.2.0 Beta 8",
"102000": "4.2.0",
"102001": "4.2.0",
"102002": "4.2.0"
"102002": "4.2.0",
"102003": "4.2.0",
"102004": "4.2.1",
"102005": "4.2.2 Beta 1",
"102006": "4.2.2 Beta 2",
"102007": "4.2.2",
"102008": "4.2.2",
"102009": "4.2.2",
"102010": "4.2.3 Beta 1",
"102011": "4.2.3",
"102012": "4.2.3",
"102013": "4.2.3",
"102014": "4.2.4 Beta 1",
"102015": "4.2.4 Beta 2",
"102016": "4.2.4",
"102017": "4.2.4",
"102018": "4.2.4",
"102019": "4.2.4",
"102020": "4.2.4",
"102021": "4.2.5 Beta 1",
"102022": "4.2.5 Beta 2",
"102023": "4.2.5 Beta 3",
"102024": "4.2.5",
"102025": "4.2.5",
"102026": "4.2.5",
"102027": "4.2.5",
"102028": "4.2.5",
"102029": "4.2.5",
"102030": "4.2.6 Beta 1",
"102031": "4.2.6 Beta 1",
"102032": "4.2.6 Beta 2",
"102033": "4.2.6 Beta 3",
"102034": "4.2.6",
"102035": "4.2.6",
"102036": "4.2.6",
"102037": "4.2.7 Beta 1",
"102038": "4.2.7 Beta 2",
"102039": "4.2.7 Beta 3",
"102040": "4.2.7 Beta 4",
"102041": "4.2.7 Beta 5",
"102042": "4.2.7",
"102043": "4.2.7",
"102044": "4.2.7",
"102045": "4.2.7",
"102046": "4.2.7",
"102047": "4.2.8",
"102048": "4.2.8",
"103000": "4.3.0 Beta 1",
"103002": "4.3.0 Beta 2",
"103003": "4.3.0 Beta 3",
"103004": "4.3.0 Beta 4",
"103005": "4.3.0"
}
+29 -1
View File
@@ -1 +1,29 @@
{"todaysBirthdays":{"class":"\\IPS\\Widget\\StaticCache","restrict":[],"default_area":"","allow_reuse":false,"menu_style":"menu","embeddable":true},"upcomingEvents":{"class":"\\IPS\\Widget\\PermissionCache","restrict":[],"default_area":"","allow_reuse":false,"menu_style":"menu","embeddable":true},"recentReviews":{"class":"\\IPS\\Widget\\PermissionCache","restrict":[],"default_area":"","allow_reuse":false,"menu_style":"menu","embeddable":true}}
{
"todaysBirthdays": {
"class": "\\IPS\\Widget\\StaticCache",
"restrict": [],
"default_area": "",
"allow_reuse": false,
"menu_style": "menu",
"embeddable": true
},
"upcomingEvents": {
"class": "\\IPS\\Widget\\PermissionCache",
"restrict": [
"sidebar",
"cms"
],
"default_area": "",
"allow_reuse": true,
"menu_style": "menu",
"embeddable": true
},
"recentReviews": {
"class": "\\IPS\\Widget\\PermissionCache",
"restrict": [],
"default_area": "",
"allow_reuse": false,
"menu_style": "menu",
"embeddable": true
}
}
@@ -30,14 +30,26 @@ class _Event
*/
public function getItems()
{
if ( \IPS\calendar\Calendar::canOnAny( 'add' ) )
if ( \IPS\calendar\Calendar::canOnAny( 'add', NULL, \IPS\Settings::i()->club_nodes_in_apps ? array() : array( array( 'cal_club_id IS NULL' ) ) ) )
{
return array( 'event' => array(
'link' => \IPS\Http\Url::internal( "app=calendar&module=calendar&controller=submit&_new=1", 'front', 'calendar_submit' ),
'extraData' => array( 'data-ipsDialog' => true, 'data-ipsDialog-size' => "narrow", ),
'title' => 'select_calendar'
)
);
if ( !\IPS\Settings::i()->club_nodes_in_apps and $theOnlyNode = \IPS\calendar\Calendar::theOnlyNode() )
{
return array(
'event' => array(
'link' => \IPS\Http\Url::internal( "app=calendar&module=calendar&controller=submit&do=submit&id=" . $theOnlyNode->_id, 'front', 'calendar_submit' ),
)
);
}
else
{
return array(
'event' => array(
'link' => \IPS\Http\Url::internal( "app=calendar&module=calendar&controller=submit&_new=1", 'front', 'calendar_submit' ),
'extraData' => array( 'data-ipsDialog' => true, 'data-ipsDialog-size' => "narrow", ),
'title' => 'select_calendar'
)
);
}
}
return array();
@@ -49,7 +49,8 @@ class _StreamItems
{
$member = \IPS\Member::load( $rsvp['rsvp_member_id'] );
$event = \IPS\calendar\Event::load( $rsvp[ 'rsvp_event_id' ] );
$rsvps[] = new \IPS\Content\Search\Result\Custom( \IPS\DateTime::ts( $rsvp[ 'rsvp_date' ] ), \IPS\Member::loggedIn()->language()->addToStack( 'calendar_activity_stream_rsvp', FALSE, array( 'htmlsprintf' => array( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLink( $member ), $event->url(), $event->title ) ) ) );
$title = htmlspecialchars( $event->title, ENT_DISALLOWED, 'UTF-8', FALSE );
$rsvps[] = new \IPS\Content\Search\Result\Custom( \IPS\DateTime::ts( $rsvp[ 'rsvp_date' ] ), \IPS\Member::loggedIn()->language()->addToStack( 'calendar_activity_stream_rsvp', FALSE, array( 'htmlsprintf' => array( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLink( $member ), $event->url(), $title ) ) ) );
}
/* Return */
@@ -38,4 +38,15 @@ class _venues extends \IPS\Node\Controller
\IPS\Dispatcher::i()->checkAcpPermission( 'venues_manage' );
parent::execute();
}
/**
* Allow overloading to change how the title is displayed in the tree
*
* @param $node \IPS\Node Node
* @return string
*/
protected static function nodeTitle( $node )
{
return $node->_title;
}
}
@@ -130,13 +130,12 @@ class _event extends \IPS\Content\Controller
$address = $location->toString();
}
/* Add JSON-LD */
\IPS\Output::i()->jsonLd['event'] = array(
'@context' => "http://schema.org",
'@type' => "Event",
'url' => (string) $this->event->url(),
'name' => htmlspecialchars( $this->event->mapped('title'), ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE ),
'name' => $this->event->mapped('title'),
'description' => strip_tags( $this->event->truncated() ),
'eventStatus' => "EventScheduled",
'startDate' => $this->event->nextOccurrence( \IPS\calendar\Date::getDate(), 'startDate' ) ?
@@ -151,7 +150,7 @@ class _event extends \IPS\Content\Controller
$this->event->lastOccurrence( 'endDate' )->format( \IPS\DateTime::ISO8601 );
}
if( $this->event->container()->allow_reviews )
if( $this->event->container()->allow_reviews AND $this->event->reviews )
{
\IPS\Output::i()->jsonLd['event']['aggregateRating'] = array(
'@type' => 'AggregateRating',
@@ -159,23 +158,6 @@ class _event extends \IPS\Content\Controller
'ratingValue' => $this->event->averageReviewRating(),
'bestRating' => \IPS\Settings::i()->reviews_rating_out_of,
);
\IPS\Output::i()->jsonLd['event']['interactionStatistic'][] = array(
'@type' => 'InteractionCounter',
'interactionType' => "http://schema.org/ReviewAction",
'userInteractionCount' => $this->event->reviews
);
}
if( $this->event->container()->allow_comments )
{
\IPS\Output::i()->jsonLd['event']['interactionStatistic'][] = array(
'@type' => 'InteractionCounter',
'interactionType' => "http://schema.org/CommentAction",
'userInteractionCount' => $this->event->mapped('num_comments')
);
\IPS\Output::i()->jsonLd['event']['commentCount'] = $this->event->mapped('num_comments');
}
if( $this->event->coverPhoto()->file )
@@ -185,12 +167,6 @@ class _event extends \IPS\Content\Controller
if( $this->event->rsvp )
{
\IPS\Output::i()->jsonLd['event']['potentialAction'] = array(
'@type' => 'RsvpAction',
'target' => (string) $this->event->url( 'rsvp' )->setQueryString( 'action', '{rsvpResponse}' ),
'rsvpResponse-input' => "required name=rsvpResponse"
);
if( count( $attendees[1] ) )
{
\IPS\Output::i()->jsonLd['event']['attendee'] = array();
@@ -199,7 +175,7 @@ class _event extends \IPS\Content\Controller
{
\IPS\Output::i()->jsonLd['event']['attendee'][] = array(
'@type' => 'Person',
'name' => htmlspecialchars( $attendee->name, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE )
'name' => $attendee->name
);
}
}
@@ -391,6 +367,12 @@ class _event extends \IPS\Content\Controller
{
\IPS\Session::i()->csrfCheck();
/* Members only */
if( !\IPS\Member::loggedIn()->member_id )
{
\IPS\Output::i()->error( 'node_error', '3L369/1', 403, '' );
}
/* Existing reminder? */
$existing = NULL;
try
@@ -403,7 +385,7 @@ class _event extends \IPS\Content\Controller
/* How far in the future is this event so we can set realistic max reminders */
$diff = $this->event->_start_date->diff( \IPS\DateTime::create() );
$max = $diff->d;
$max = $diff->days;
$form = new \IPS\Helpers\Form;
$form->class = 'ipsForm_vertical';
@@ -30,45 +30,46 @@ class _submit extends \IPS\Dispatcher\Controller
*/
protected function manage()
{
$form = new \IPS\Helpers\Form( 'select_calendar', 'continue' );
$form->class = 'ipsForm_vertical ipsForm_noLabels';
$form->add( new \IPS\Helpers\Form\Node( 'calendar', NULL, TRUE, array(
'url' => \IPS\Http\Url::internal( 'app=calendar&module=calendar&controller=submit', 'front', 'calendar_submit' ),
'class' => 'IPS\calendar\Calendar',
'permissionCheck' => 'add',
'forceOwner' => \IPS\Member::loggedIn(),
) ) );
$form = new \IPS\Helpers\Form( 'select_calendar', 'continue' );
$form->class = 'ipsForm_vertical ipsForm_noLabels';
$form->add( new \IPS\Helpers\Form\Node( 'calendar', NULL, TRUE, array(
'url' => \IPS\Http\Url::internal( 'app=calendar&module=calendar&controller=submit', 'front', 'calendar_submit' ),
'class' => 'IPS\calendar\Calendar',
'permissionCheck' => 'add',
'forceOwner' => \IPS\Member::loggedIn(),
'clubs' => \IPS\Settings::i()->club_nodes_in_apps
) ) );
/* Are we creating an event for a specific day? If yes, pass the values to the form */
if( \IPS\Request::i()->y AND \IPS\Request::i()->m AND \IPS\Request::i()->d )
/* Are we creating an event for a specific day? If yes, pass the values to the form */
if( \IPS\Request::i()->y AND \IPS\Request::i()->m AND \IPS\Request::i()->d )
{
$form->hiddenValues['y'] = \IPS\Request::i()->y;
$form->hiddenValues['m'] = \IPS\Request::i()->m;
$form->hiddenValues['d'] = \IPS\Request::i()->d;
}
/* Are we coming from a venue? */
if( \IPS\Settings::i()->calendar_venues_enabled and \IPS\Request::i()->venue )
{
$form->hiddenValues['venue'] = \IPS\Request::i()->venue;
}
if ( $values = $form->values() )
{
$url = \IPS\Http\Url::internal( 'app=calendar&module=calendar&controller=submit&do=submit', 'front', 'calendar_submit' )->setQueryString( 'id', $values['calendar']->_id );
if( isset( $values['y'], $values['m'], $values['d'] ) )
{
$form->hiddenValues['y'] = \IPS\Request::i()->y;
$form->hiddenValues['m'] = \IPS\Request::i()->m;
$form->hiddenValues['d'] = \IPS\Request::i()->d;
$url = $url->setQueryString( 'd', $values['d'] )->setQueryString( 'm', $values['m'] )->setQueryString( 'y', $values['y'] );
}
/* Are we coming from a venue? */
if( \IPS\Settings::i()->calendar_venues_enabled and \IPS\Request::i()->venue )
if( \IPS\Settings::i()->calendar_venues_enabled and isset( $values['venue'] ) )
{
$form->hiddenValues['venue'] = \IPS\Request::i()->venue;
$url = $url->setQueryString( 'venue', $values['venue'] );
}
if ( $values = $form->values() )
{
$url = \IPS\Http\Url::internal( 'app=calendar&module=calendar&controller=submit&do=submit', 'front', 'calendar_submit' )->setQueryString( 'id', $values['calendar']->_id );
if( isset( $values['y'], $values['m'], $values['d'] ) )
{
$url = $url->setQueryString( 'd', $values['d'] )->setQueryString( 'm', $values['m'] )->setQueryString( 'y', $values['y'] );
}
if( \IPS\Settings::i()->calendar_venues_enabled and isset( $values['venue'] ) )
{
$url = $url->setQueryString( 'venue', $values['venue'] );
}
\IPS\Output::i()->redirect( $url );
}
\IPS\Output::i()->redirect( $url );
}
/* Display */
\IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack( 'submit_event' );
@@ -138,19 +139,15 @@ class _submit extends \IPS\Dispatcher\Controller
}
catch ( \OutOfRangeException $e )
{
\IPS\Output::i()->redirect( IPS\Http\Url::internal( 'app=calendar&module=calendar&controller=submit', 'front', 'calendar_submit' ) );
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=calendar&module=calendar&controller=submit', 'front', 'calendar_submit' ) );
}
/* Are we the author of the existing event? */
if( $existing->author() !== \IPS\Member::loggedIn() )
{
\IPS\Output::i()->redirect( IPS\Http\Url::internal( 'app=calendar&module=calendar&controller=submit', 'front', 'calendar_submit' ) );
\IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=calendar&module=calendar&controller=submit', 'front', 'calendar_submit' ) );
}
/* Reset elements we don't want to copy */
$existing->start_date = NULL;
$existing->end_date = NULL;
$form = new \IPS\Helpers\Form( 'form', \IPS\Member::loggedIn()->language()->checkKeyExists( \IPS\Calendar\Event::$formLangPrefix . '_save' ) ? \IPS\Calendar\Event::$formLangPrefix . '_save' : 'save' );
$form->class = 'ipsForm_vertical';
$formElements = \IPS\Calendar\Event::formElements( $existing, $existing->container() );
@@ -168,6 +165,12 @@ class _submit extends \IPS\Dispatcher\Controller
if ( $values = $form->values() )
{
/* Set the container */
if ( !isset( $values[ 'event_container' ] ) )
{
$values[ 'event_container' ] = $existing->container();
}
/* Disable read/write separation */
\IPS\Db::i()->readWriteSeparation = FALSE;
@@ -73,7 +73,7 @@ class _venue extends \IPS\Dispatcher\Controller
}
catch( \InvalidArgumentException $e )
{
\IPS\Output::i()->error( 'error_bad_date', '', 403, '' ); //@todo
\IPS\Output::i()->error( 'error_bad_date', '2L354/2', 403, '' );
}
$upcoming = \IPS\calendar\Event::retrieveEvents(
@@ -107,7 +107,7 @@ class _venue extends \IPS\Dispatcher\Controller
'@context' => "http://schema.org",
'@type' => "EventVenue",
'url' => (string) $this->venue->url(),
'name' => htmlspecialchars( $this->venue->_title, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE )
'name' => $this->venue->_title
);
\IPS\Output::i()->title = $this->venue->_title;
@@ -52,7 +52,7 @@ class _view extends \IPS\Dispatcher\Controller
}
/* Is there only one calendar? */
$roots = \IPS\calendar\Calendar::roots();
$roots = \IPS\Settings::i()->club_nodes_in_apps ? \IPS\calendar\Calendar::rootsWithClubs() : \IPS\calendar\Calendar::roots();
if ( count( $roots ) == 1 AND !isset( \IPS\Request::i()->id ) )
{
$roots = array_shift( $roots );
@@ -96,6 +96,11 @@ class _view extends \IPS\Dispatcher\Controller
/* Get the date jumper - do this first in case we need to redirect */
$jump = $this->_jump();
if( !\IPS\Request::i()->isAjax() )
{
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'front_browse.js', 'calendar', 'front' ) );
}
/* If there is a view requested in the URL, use it */
if( isset( \IPS\Request::i()->view ) )
{
@@ -118,7 +123,7 @@ class _view extends \IPS\Dispatcher\Controller
}
/* Online User Location */
if ($this->_calendar)
if ( $this->_calendar )
{
\IPS\Session::i()->setLocation( $this->_calendar->url(), array(), 'loc_calendar_viewing_calendar', array( "calendar_calendar_{$this->_calendar->id}" => TRUE ) );
}
@@ -138,7 +143,7 @@ class _view extends \IPS\Dispatcher\Controller
protected function _viewMonth( $jump, $featured=array() )
{
/* Get the calendars we can view */
$calendars = \IPS\calendar\Calendar::roots();
$calendars = \IPS\Settings::i()->club_nodes_in_apps ? \IPS\calendar\Calendar::rootsWithClubs() : \IPS\calendar\Calendar::roots();
/* Get the month data */
$day = NULL;
@@ -164,7 +169,7 @@ class _view extends \IPS\Dispatcher\Controller
}
catch( \Exception $e )
{
\IPS\Output::i()->error( 'error_bad_date', '3L182/3', 403, '' );
\IPS\Output::i()->error( 'error_bad_date', '2L182/7', 403, '' );
}
/* If there are no events, tell search engines not to index the page but do NOT tell them not to follow links */
@@ -182,7 +187,6 @@ class _view extends \IPS\Dispatcher\Controller
}
else
{
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'front_browse.js', 'calendar', 'front' ) );
\IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack('cal_month_title', FALSE, array( 'sprintf' => array( $date->monthName, $date->year ) ) );
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'browse' )->calendarWrapper(
@@ -207,7 +211,7 @@ class _view extends \IPS\Dispatcher\Controller
protected function _viewWeek( $jump, $featured=array() )
{
/* Get the calendars we can view */
$calendars = \IPS\calendar\Calendar::roots();
$calendars = \IPS\Settings::i()->club_nodes_in_apps ? \IPS\calendar\Calendar::rootsWithClubs() : \IPS\calendar\Calendar::roots();
/* Get the month data */
$week = \IPS\Request::i()->w ? explode( '-', \IPS\Request::i()->w ) : NULL;
@@ -217,32 +221,39 @@ class _view extends \IPS\Dispatcher\Controller
}
catch( \Exception $e )
{
\IPS\Output::i()->error( 'error_bad_date', '3L182/4', 403, '' );
\IPS\Output::i()->error( 'error_bad_date', '2L182/8', 403, '' );
}
$nextWeek = $date->adjust( '+1 week' );
$lastWeek = $date->adjust( '-1 week' );
/* Get the days of the week - we do this in PHP to help keep template a little cleaner */
$days = array();
for( $i = 0; $i < 7; $i++ )
try
{
$days[] = \IPS\calendar\Date::getDate( $date->firstDayOfWeek('year'), $date->firstDayOfWeek('mon'), $date->firstDayOfWeek('mday') )->adjust( $i . ' days' );
$days = array();
for( $i = 0; $i < 7; $i++ )
{
$days[] = \IPS\calendar\Date::getDate( $date->firstDayOfWeek('year'), $date->firstDayOfWeek('mon'), $date->firstDayOfWeek('mday') )->adjust( $i . ' days' );
}
/* Get birthdays */
$birthdays = ( $this->_calendar === NULL OR count( $calendars ) == 1 ) ? $date->getBirthdays( FALSE, TRUE ) : array();
/* Get the events within this range */
$events = \IPS\calendar\Event::retrieveEvents(
\IPS\calendar\Date::getDate( $date->firstDayOfWeek('year'), $date->firstDayOfWeek('mon'), $date->firstDayOfWeek('mday') ),
\IPS\calendar\Date::getDate( $date->lastDayOfWeek('year'), $date->lastDayOfWeek('mon'), $date->lastDayOfWeek('mday'), 23, 59, 59 ),
$this->_calendar
);
}
catch( \Exception $e )
{
\IPS\Output::i()->error( 'error_bad_date', '2L182/C', 403, '' );
}
/* Get birthdays */
$birthdays = ( $this->_calendar === NULL OR count( $calendars ) == 1 ) ? $date->getBirthdays( FALSE, TRUE ) : array();
/* Get the events within this range */
$events = \IPS\calendar\Event::retrieveEvents(
\IPS\calendar\Date::getDate( $date->firstDayOfWeek('year'), $date->firstDayOfWeek('mon'), $date->firstDayOfWeek('mday') ),
\IPS\calendar\Date::getDate( $date->lastDayOfWeek('year'), $date->lastDayOfWeek('mon'), $date->lastDayOfWeek('mday'), 23, 59, 59 ),
$this->_calendar
);
/* If there are no events, tell search engines not to index the page but do NOT tell them not to follow links */
if( count($events) === 0 )
if( count( $events ) === 0 )
{
\IPS\Output::i()->metaTags['robots'] = 'noindex';
}
@@ -287,7 +298,7 @@ class _view extends \IPS\Dispatcher\Controller
protected function _viewDay( $jump, $featured=array() )
{
/* Get the calendars we can view */
$calendars = \IPS\calendar\Calendar::roots();
$calendars = \IPS\Settings::i()->club_nodes_in_apps ? \IPS\calendar\Calendar::rootsWithClubs() : \IPS\calendar\Calendar::roots();
/* Get the month data */
try
@@ -296,7 +307,7 @@ class _view extends \IPS\Dispatcher\Controller
}
catch( \Exception $e )
{
\IPS\Output::i()->error( 'error_bad_date', '3L182/5', 403, '' );
\IPS\Output::i()->error( 'error_bad_date', '2L182/9', 403, '' );
}
$tomorrow = $date->adjust( '+1 day' );
@@ -312,6 +323,7 @@ class _view extends \IPS\Dispatcher\Controller
$table->rowsTemplate = array( \IPS\Theme::i()->getTemplate( 'browse', 'calendar', 'front' ), 'birthday' );
\IPS\Output::i()->output = $table;
\IPS\Output::i()->metaTags['robots'] = 'noindex';
return;
}
@@ -394,7 +406,7 @@ class _view extends \IPS\Dispatcher\Controller
protected function _viewStream( $jump, $featured=array() )
{
/* Get the calendars we can view */
$calendars = \IPS\calendar\Calendar::roots();
$calendars = \IPS\Settings::i()->club_nodes_in_apps ? \IPS\calendar\Calendar::rootsWithClubs() : \IPS\calendar\Calendar::roots();
/* Get the month data */
$day = NULL;
@@ -407,21 +419,21 @@ class _view extends \IPS\Dispatcher\Controller
try
{
$date = \IPS\calendar\Date::getDate( \IPS\Request::i()->y ?: NULL, \IPS\Request::i()->m ?: NULL, $day );
/* Get the events within this range */
$events = \IPS\calendar\Event::retrieveEvents(
\IPS\calendar\Date::getDate( $date->firstDayOfMonth('year'), $date->firstDayOfMonth('mon'), $date->firstDayOfMonth('mday') ),
\IPS\calendar\Date::getDate( $date->lastDayOfMonth('year'), $date->lastDayOfMonth('mon'), $date->lastDayOfMonth('mday'), 23, 59, 59 ),
$this->_calendar,
NULL,
FALSE
);
}
catch( \InvalidArgumentException $e )
{
\IPS\Output::i()->error( 'error_bad_date', '3L182/6', 403, '' );
\IPS\Output::i()->error( 'error_bad_date', '2L182/A', 403, '' );
}
/* Get the events within this range */
$events = \IPS\calendar\Event::retrieveEvents(
\IPS\calendar\Date::getDate( $date->firstDayOfMonth('year'), $date->firstDayOfMonth('mon'), $date->firstDayOfMonth('mday') ),
\IPS\calendar\Date::getDate( $date->lastDayOfMonth('year'), $date->lastDayOfMonth('mon'), $date->lastDayOfMonth('mday'), 23, 59, 59 ),
$this->_calendar,
NULL,
FALSE
);
/* Sort */
$startDate = \IPS\calendar\Date::getDate();
@usort( $events, function( $a, $b ) use ( $startDate )
@@ -457,7 +469,7 @@ class _view extends \IPS\Dispatcher\Controller
}
/* If there are no events, tell search engines not to index the page but do NOT tell them not to follow links */
if( count($events) === 0 )
if( count( $events ) === 0 )
{
\IPS\Output::i()->metaTags['robots'] = 'noindex';
}
@@ -466,6 +478,12 @@ class _view extends \IPS\Dispatcher\Controller
$events = array_slice( $events, ( $pagination['page'] - 1 ) * $this->streamPerPage, $this->streamPerPage );
}
/* Non-existent page */
if( $pagination['page'] > 1 AND !count( $events ) )
{
\IPS\Output::i()->error( 'no_events_month', '2L182/B', 404, '' );
}
/* Display */
$output = \IPS\Theme::i()->getTemplate( 'browse' )->calendarStream( $calendars, $date, $featured, $events, $this->_calendar, $jump, $pagination );
@@ -0,0 +1,37 @@
<?php
/**
* @brief 4.2.2 Beta 1 Upgrade Code
* @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 Calendar
* @since 26 Jul 2017
*/
namespace IPS\calendar\setup\upg_102005;
/* 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;
}
/**
* 4.2.2 Beta 1 Upgrade Code
*/
class _Upgrade
{
/**
* Recount containers
*
* @return array If returns TRUE, upgrader will proceed to next step. If it returns any other value, it will set this as the value of the 'extra' GET parameter and rerun this step (useful for loops)
*/
public function finish()
{
\IPS\Task::queue( 'core', 'RebuildContainerCounts', array( 'class' => 'IPS\calendar\Calendar', 'count' => 0 ), 5, array( 'class' ) );
return TRUE;
}
}
@@ -0,0 +1,79 @@
{
"1": {
"method": "addColumn",
"params": [
"calendar_event_reviews",
{
"name": "review_author_response",
"type": "MEDIUMTEXT",
"length": null,
"decimals": null,
"values": null,
"allow_null": true,
"default": null,
"comment": "",
"unsigned": false,
"zerofill": false,
"auto_increment": false,
"binary": false
}
]
},
"2": {
"method": "changeIndex",
"params": [
"calendar_event_reviews",
"review_mid",
{
"type": "key",
"name": "review_mid",
"columns": [
"review_mid",
"review_date"
],
"length": [
null,
null
]
}
]
},
"3": {
"method": "addIndex",
"params": [
"calendar_event_rsvp",
{
"type": "key",
"name": "rsvp_search",
"columns": [
"rsvp_date",
"rsvp_member_id"
],
"length": [
null,
null
]
}
]
},
"4": {
"method": "addIndex",
"params": [
"calendar_events",
{
"type": "key",
"name": "event_album",
"columns": [
"event_album",
"event_approved",
"event_saved"
],
"length": [
null,
null,
null
]
}
]
}
}
+113 -12
View File
@@ -69,9 +69,9 @@ class _Date extends \IPS\DateTime
public static $dateFormats = array(
'locale' => "%x",
'd_sm_y' => "%d %b %Y",
'd_lm_y' => "%d %B %Y",
'd_lm_y' => "%d {monthName} %Y",
'sm_d_y' => "%b %d, %Y",
'lm_d_y' => "%B %d, %Y",
'lm_d_y' => "{monthName} %d, %Y",
);
/**
@@ -127,7 +127,7 @@ class _Date extends \IPS\DateTime
/* Store the information and return the object */
$obj = static::ts( $timeStamp - $offset );
$obj->dateInformation = getdate( $timeStamp );
$obj->getDateInformation( $timeStamp );
$obj->offset = $offset;
return $obj;
@@ -152,7 +152,7 @@ class _Date extends \IPS\DateTime
$result = parent::__construct( $time );
}
$this->dateInformation = getdate( $this->getTimestamp() );
$this->getDateInformation( $this->getTimestamp() );
$this->offset = $this->getOffset();
return $result;
@@ -169,12 +169,36 @@ class _Date extends \IPS\DateTime
{
$result = parent::setTimezone( $timezone );
$this->dateInformation = getdate( $this->getTimestamp() );
$this->getDateInformation( $this->getTimestamp() );
$this->offset = $this->getOffset();
return $result;
}
/**
* Get the date information
*
* @note This is basically a timezone aware wrapper for getdate
* @param int $time Timestamp
* @return void
*/
public function getDateInformation( $time )
{
$this->dateInformation = array(
'seconds' => (int) $this->strFormat( '%S' ),
'minutes' => (int) $this->strFormat( '%M' ),
'hours' => (int) $this->strFormat( '%H' ),
'mday' => (int) $this->strFormat( '%d' ),
'wday' => (int) $this->strFormat( '%w' ),
'mon' => (int) $this->strFormat( '%m' ),
'year' => (int) $this->strFormat( '%Y' ),
'yday' => (int) $this->strFormat( '%j' ) - 1,
'weekday' => $this->strFormat( '%A' ),
'month' => $this->strFormat( '%B' ),
0 => $time
);
}
/**
* Returns a date object created based on an arbitrary string. Used for both relative time strings and SQL datetime values.
*
@@ -186,7 +210,47 @@ class _Date extends \IPS\DateTime
public static function parseTime( $datetime, $forceUTC=FALSE )
{
/* Create an \IPS\DateTime object from the datetime value passed in */
$timezone = ( $forceUTC === TRUE ) ? new \DateTimeZone( 'UTC' ) : ( \IPS\Member::loggedIn()->timezone ? new \DateTimeZone( \IPS\Member::loggedIn()->timezone ) : NULL );
if ( !$forceUTC AND \IPS\Dispatcher::hasInstance() )
{
if( !\IPS\Member::loggedIn()->timezone )
{
$timezone = NULL;
}
else
{
$validTimezone = TRUE;
if( in_array( \IPS\Member::loggedIn()->timezone, static::getTimezoneIdentifiers() ) )
{
try
{
$timezone = new \DateTimeZone( \IPS\Member::loggedIn()->timezone );
}
catch ( \Exception $e )
{
$validTimezone = FALSE;
}
}
else
{
$validTimezone = FALSE;
}
if( ! $validTimezone )
{
\IPS\Member::loggedIn()->timezone = null;
if ( \IPS\Member::loggedIn()->member_id )
{
\IPS\Member::loggedIn()->save();
}
}
}
}
else
{
$timezone = new \DateTimeZone( 'UTC' );
}
$datetime = new \IPS\DateTime( $datetime, $timezone );
/* Now correct it back if necessary */
@@ -444,7 +508,7 @@ class _Date extends \IPS\DateTime
}
elseif( isset( static::$dateFormats[ \IPS\Settings::i()->calendar_date_format ] ) )
{
return static::$dateFormats[ \IPS\Settings::i()->calendar_date_format ];
return str_replace( '{monthName}', static::setMonthModifier(), static::$dateFormats[ \IPS\Settings::i()->calendar_date_format ] );
}
else
{
@@ -455,18 +519,25 @@ class _Date extends \IPS\DateTime
/**
* Return the MySQL-style datetime value
*
* @param bool $includeTime Whether to include time or not
* @param bool $includeTime Whether to include time or not
* @param bool $timezoned Whether to adjust for timezone or not. This will usually be FALSE for database purposes, but set to TRUE if you're using this to compare if two dates are on the same day
* @return string
*/
public function mysqlDatetime( $includeTime=TRUE )
public function mysqlDatetime( $includeTime=TRUE, $timezoned = FALSE )
{
$timeStamp = isset( $this->dateInformation[0] ) ? $this->dateInformation[0] : $this->getTimestamp();
if ( $timezoned )
{
$timeStamp += $this->getTimezone()->getOffset( $this );
}
if( $includeTime )
{
return date( 'Y-m-d H:i:s', isset( $this->dateInformation[0] ) ? $this->dateInformation[0] : $this->getTimestamp() );
return date( 'Y-m-d H:i:s', $timeStamp );
}
else
{
return date( 'Y-m-d', isset( $this->dateInformation[0] ) ? $this->dateInformation[0] : $this->getTimestamp() );
return date( 'Y-m-d', $timeStamp );
}
}
@@ -756,7 +827,7 @@ class _Date extends \IPS\DateTime
if( $key == 'monthName' AND isset( $data[0] ) )
{
return mb_convert_case( \IPS\Member::loggedIn()->language()->convertString( strftime( '%B', $data[0] ) ), MB_CASE_TITLE );
return mb_convert_case( \IPS\Member::loggedIn()->language()->convertString( strftime( static::setMonthModifier(), $data[0] ) ), MB_CASE_TITLE );
}
if( $key == 'monthNameShort' AND isset( $data[0] ) )
@@ -777,6 +848,36 @@ class _Date extends \IPS\DateTime
return NULL;
}
/**
* @brief strftime-compatible modifier for month name
*/
protected static $monthNameModifier = NULL;
/**
* Sets the month name modifier
*
* @note Some languages on some OS's (e.g. FreeBSD) require using a different modifier for full month name
* @return void
*/
static protected function setMonthModifier()
{
if( static::$monthNameModifier === NULL )
{
$test = @strftime( '%OB' );
if( $test === FALSE OR $test == '%OB' )
{
static::$monthNameModifier = '%B';
}
else
{
static::$monthNameModifier = '%OB';
}
}
return static::$monthNameModifier;
}
/**
* Format the time according to the user's locale (without the date)
*
@@ -21,9 +21,9 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
/**
* Event Comment Model
*/
class _Comment extends \IPS\Content\Comment implements \IPS\Content\EditHistory, \IPS\Content\ReportCenter, \IPS\Content\Hideable, \IPS\Content\Searchable, \IPS\Content\Embeddable
class _Comment extends \IPS\Content\Comment implements \IPS\Content\EditHistory, \IPS\Content\Hideable, \IPS\Content\Searchable, \IPS\Content\Embeddable
{
use \IPS\Content\Reactable;
use \IPS\Content\Reactable, \IPS\Content\Reportable;
/**
* @brief [ActiveRecord] Multiton Store
@@ -99,7 +99,7 @@ class _Comment extends \IPS\Content\Comment implements \IPS\Content\EditHistory,
*/
public static function contentTableTemplate()
{
return array( \IPS\Theme::i()->getTemplate( 'tables', 'core' ), 'commentRows' );
return array( \IPS\Theme::i()->getTemplate( 'tables', 'core', 'front' ), 'commentRows' );
}
/**
+132 -36
View File
@@ -25,7 +25,6 @@ class _Event extends \IPS\Content\Item implements
\IPS\Content\Permissions,
\IPS\Content\Tags,
\IPS\Content\Followable,
\IPS\Content\ReportCenter,
\IPS\Content\ReadMarkers,
\IPS\Content\Hideable, \IPS\Content\Featurable, \IPS\Content\Lockable,
\IPS\Content\Shareable,
@@ -33,7 +32,7 @@ class _Event extends \IPS\Content\Item implements
\IPS\Content\Embeddable,
\IPS\Content\MetaData
{
use \IPS\Content\Reactable;
use \IPS\Content\Reactable, \IPS\Content\Reportable;
/**
* @brief Application
@@ -124,6 +123,11 @@ class _Event extends \IPS\Content\Item implements
* @brief Cover Photo Storage Extension
*/
public static $coverPhotoStorageExtension = 'calendar_Events';
/**
* @brief Use a default cover photo
*/
public static $coverPhotoDefault = true;
/**
* @brief Cached date objects
@@ -134,6 +138,11 @@ class _Event extends \IPS\Content\Item implements
* @brief Cached venue
*/
protected $venueObject = NULL;
/**
* @brief Location Data Cache
*/
protected $locationData = NULL;
/**
* @brief [Content] Key for hide reasons
@@ -546,19 +555,19 @@ class _Event extends \IPS\Content\Item implements
/* Do we have an occurrences limit? */
if( $recurringData['repeat_end_occurrences'] )
{
while( $occurrences < $recurringData['repeat_end_occurrences'] )
$period = new \DatePeriod( $date, new \DateInterval( 'P' . $recurringData['event_repeat_freq'] . mb_strtoupper( mb_substr( $keyword, 0, 1 ) ) ), $recurringData['repeat_end_occurrences'] );
foreach( $period as $dateOccurrence )
{
if( ( $date->mysqlDatetime( FALSE ) >= $startDate->mysqlDatetime( FALSE ) AND $date->mysqlDatetime( FALSE ) <= $endDate->mysqlDatetime( FALSE ) ) OR
if( ( $dateOccurrence->mysqlDatetime( FALSE ) >= $startDate->mysqlDatetime( FALSE ) AND $dateOccurrence->mysqlDatetime( FALSE ) <= $endDate->mysqlDatetime( FALSE ) ) OR
( $eDate !== NULL AND $eDate->mysqlDatetime( FALSE ) >= $startDate->mysqlDatetime( FALSE ) AND $eDate->mysqlDatetime( FALSE ) <= $endDate->mysqlDatetime( FALSE ) ) OR
( $eDate !== NULL AND $date->mysqlDatetime( FALSE ) <= $startDate->mysqlDatetime( FALSE ) AND $eDate->mysqlDatetime( FALSE ) >= $endDate->mysqlDatetime( FALSE ) )
)
{
$results[] = array( 'startDate' => $date, 'endDate' => $eDate );
$results[] = array( 'startDate' => new \IPS\calendar\Date( $dateOccurrence->mysqlDatetime( FALSE, TRUE ) ), 'endDate' => $eDate );
}
$date = $date->adjust( "+{$recurringData['event_repeat_freq']} {$keyword}" );
$eDate = ( $eDate !== NULL ) ? $eDate->adjust( "+{$recurringData['event_repeat_freq']} {$keyword}" ) : NULL;
$occurrences++;
}
}
/* Do we have an end date for the recurrences? */
@@ -566,34 +575,36 @@ class _Event extends \IPS\Content\Item implements
{
$endDateToUse = ( $endDate->mysqlDatetime( FALSE ) < $recurringData['repeat_end_date']->mysqlDatetime( FALSE ) ) ? $endDate : $recurringData['repeat_end_date'];
while( $date->mysqlDatetime( FALSE ) <= $endDateToUse->mysqlDatetime( FALSE ) )
$period = new \DatePeriod( $date, new \DateInterval( 'P' . $recurringData['event_repeat_freq'] . mb_strtoupper( mb_substr( $keyword, 0, 1 ) ) ), $endDateToUse );
foreach( $period as $dateOccurrence )
{
if( ( $date->mysqlDatetime( FALSE ) >= $startDate->mysqlDatetime( FALSE ) AND $date->mysqlDatetime( FALSE ) <= $endDate->mysqlDatetime( FALSE ) ) OR
if( ( $dateOccurrence->mysqlDatetime( FALSE ) >= $startDate->mysqlDatetime( FALSE ) AND $dateOccurrence->mysqlDatetime( FALSE ) <= $endDate->mysqlDatetime( FALSE ) ) OR
( $eDate !== NULL AND $eDate->mysqlDatetime( FALSE ) >= $startDate->mysqlDatetime( FALSE ) AND $eDate->mysqlDatetime( FALSE ) <= $endDate->mysqlDatetime( FALSE ) ) OR
( $eDate !== NULL AND $date->mysqlDatetime( FALSE ) <= $startDate->mysqlDatetime( FALSE ) AND $eDate->mysqlDatetime( FALSE ) >= $endDate->mysqlDatetime( FALSE ) )
)
{
$results[] = array( 'startDate' => $date, 'endDate' => $eDate );
$results[] = array( 'startDate' => new \IPS\calendar\Date( $dateOccurrence->mysqlDatetime( FALSE, TRUE ) ), 'endDate' => $eDate );
}
$date = $date->adjust( "+{$recurringData['event_repeat_freq']} {$keyword}" );
$eDate = ( $eDate !== NULL ) ? $eDate->adjust( "+{$recurringData['event_repeat_freq']} {$keyword}" ) : NULL;
}
}
/* Recurs indefinitely... the most fun type... */
else
{
while( $date->mysqlDatetime( FALSE ) <= $endDate->mysqlDatetime( FALSE ) )
$period = new \DatePeriod( $date, new \DateInterval( 'P' . $recurringData['event_repeat_freq'] . mb_strtoupper( mb_substr( $keyword, 0, 1 ) ) ), $endDate );
foreach( $period as $dateOccurrence )
{
if( ( $date->mysqlDatetime( FALSE ) >= $startDate->mysqlDatetime( FALSE ) AND $date->mysqlDatetime( FALSE ) <= $endDate->mysqlDatetime( FALSE ) ) OR
if( ( $dateOccurrence->mysqlDatetime( FALSE ) >= $startDate->mysqlDatetime( FALSE ) AND $dateOccurrence->mysqlDatetime( FALSE ) <= $endDate->mysqlDatetime( FALSE ) ) OR
( $eDate !== NULL AND $eDate->mysqlDatetime( FALSE ) >= $startDate->mysqlDatetime( FALSE ) AND $eDate->mysqlDatetime( FALSE ) <= $endDate->mysqlDatetime( FALSE ) ) OR
( $eDate !== NULL AND $date->mysqlDatetime( FALSE ) <= $startDate->mysqlDatetime( FALSE ) AND $eDate->mysqlDatetime( FALSE ) >= $endDate->mysqlDatetime( FALSE ) )
)
{
$results[] = array( 'startDate' => $date, 'endDate' => $eDate );
$results[] = array( 'startDate' => new \IPS\calendar\Date( $dateOccurrence->mysqlDatetime( FALSE, TRUE ) ), 'endDate' => $eDate );
}
$date = $date->adjust( "+{$recurringData['event_repeat_freq']} {$keyword}" );
$eDate = ( $eDate !== NULL ) ? $eDate->adjust( "+{$recurringData['event_repeat_freq']} {$keyword}" ) : NULL;
}
}
@@ -622,7 +633,7 @@ class _Event extends \IPS\Content\Item implements
{
foreach( $this->parsedOccurrences as $occurrence )
{
if( $occurrence[ $type ] AND $occurrence[ $type ]->mysqlDatetime( FALSE ) >= $date->mysqlDatetime( FALSE ) )
if( $occurrence[ $type ] AND $occurrence[ $type ]->mysqlDatetime( FALSE, TRUE ) >= $date->mysqlDatetime( FALSE, TRUE ) )
{
return $occurrence[ $type ];
}
@@ -632,7 +643,7 @@ class _Event extends \IPS\Content\Item implements
/* Get the occurrences over the next year then and try from there. We go back one month to start with to account for the event stream. */
foreach( $this->findOccurrences( $date->adjust( "-1 month" ), $date->adjust( "+2 years" ) ) as $occurrence )
{
if( $occurrence[ $type ] AND $occurrence[ $type ]->mysqlDatetime( FALSE ) >= $date->mysqlDatetime( FALSE ) )
if( $occurrence[ $type ] AND $occurrence[ $type ]->mysqlDatetime( FALSE, TRUE ) >= $date->mysqlDatetime( FALSE, TRUE ) )
{
return $occurrence[ $type ];
}
@@ -905,9 +916,9 @@ class _Event extends \IPS\Content\Item implements
*/
public static function contentTableTemplate()
{
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'calendar.css', 'calendar' ) );
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'calendar_responsive.css', 'calendar' ) );
return array( \IPS\Theme::i()->getTemplate( 'global', 'calendar' ), 'rows' );
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'calendar.css', 'calendar', 'front' ) );
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'calendar_responsive.css', 'calendar', 'front' ) );
return array( \IPS\Theme::i()->getTemplate( 'global', 'calendar', 'front' ), 'rows' );
}
/**
@@ -1011,7 +1022,7 @@ class _Event extends \IPS\Content\Item implements
{
if ( $container and $container->moderate and !$member->group['g_avoid_q'] )
{
return TRUE;
return !static::modPermission( 'approve', $member, $container );
}
return parent::moderateNewItems( $member, $container );
@@ -1086,6 +1097,15 @@ class _Event extends \IPS\Content\Item implements
'repeat_end_date' => NULL, /* Ends on x date (which is separate from the event end date - e.g. jan 9 2014 3pm to jan 10 2014 3pm, repeat annually until jan 9 2019) */
);
/* If we're copying an event, reset the date & time fields */
if ( \IPS\Request::i()->do === 'copy' )
{
$dateValues['start_date'] = NULL;
$dateValues['start_time'] = NULL;
$dateValues['end_date'] = NULL;
$dateValues['end_time'] = NULL;
}
foreach( \IPS\calendar\Date::getDayNames() as $day )
{
$dateValues['repeat_freq_on_' . $day['ical'] ] = NULL; /* If repeating weekly, this is the days of the week as checkboxes (e.g. repeat every wed, fri and sat) */
@@ -1112,6 +1132,11 @@ class _Event extends \IPS\Content\Item implements
$val['start_time'] = str_replace( '.', ':', $val['start_time'] );
}
if( isset( $val['no_end_time'] ) AND $val['no_end_time'] AND isset( $val['end_time'] ) )
{
unset( $val['end_time'] );
}
if( isset( $val['end_time'] ) )
{
$val['end_time'] = str_replace( '.', ':', $val['end_time'] );
@@ -1192,7 +1217,7 @@ class _Event extends \IPS\Content\Item implements
}, NULL, NULL, 'event_dates' );
/* Init */
if ( !$container or !$container->club() )
if ( ( !$container or !$container->club() ) AND !\IPS\calendar\Calendar::theOnlyNode() )
{
$return['calendar'] = new \IPS\Helpers\Form\Node( static::$formLangPrefix . 'container', $container ? $container->id : ( isset( \IPS\Request::i()->calendar ) ? \IPS\Request::i()->calendar : NULL ), TRUE, array(
'url' => \IPS\Http\Url::internal( 'app=calendar&module=calendar&controller=submit', 'front', 'calendar_submit' ),
@@ -1207,7 +1232,7 @@ class _Event extends \IPS\Content\Item implements
$return = array_merge( $return, parent::formElements( $item, $container ) );
/* Event description */
$return['description'] = new \IPS\Helpers\Form\Editor( 'event_content', $item ? $item->content : NULL, TRUE, array( 'app' => 'calendar', 'key' => 'Calendar', 'autoSaveKey' => 'calendar-event', 'attachIds' => ( $item === NULL ? NULL : array( $item->id ) ) ) );
$return['description'] = new \IPS\Helpers\Form\Editor( 'event_content', $item ? $item->content : NULL, TRUE, array( 'app' => 'calendar', 'key' => 'Calendar', 'autoSaveKey' => 'calendar-event', 'attachIds' => ( $item === NULL ? NULL : array( $item->id, NULL, 'description' ) ) ) );
/* Cover photo and location */
$return['header'] = new \IPS\Helpers\Form\Upload( 'event_cover_photo', ( $item AND $item->cover_photo ) ? \IPS\File::get( 'calendar_Events', $item->cover_photo ) : NULL, FALSE, array( 'image' => TRUE, 'storageExtension' => 'calendar_Events' ) );
@@ -1250,6 +1275,12 @@ class _Event extends \IPS\Content\Item implements
$return['rsvplimit'] = new \IPS\Helpers\Form\Number( 'event_rsvp_limit', $item ? $item->rsvp_limit : -1, FALSE, array( 'unlimited' => -1 ), NULL, NULL, NULL, 'event_rsvp_limit' );
}
/* If the calendar does not allow comments or reviews, disable the auto follow option */
if( $container AND !$container->allow_comments AND !$container->allow_reviews )
{
unset( $return['auto_follow'] );
}
return $return;
}
@@ -1262,7 +1293,7 @@ class _Event extends \IPS\Content\Item implements
protected function processBeforeCreate( $values )
{
/* Post key is very much needed because...bacon */
$this->post_key = isset( $values['post_key'] ) ? $values['post_key'] : md5( uniqid() );
$this->post_key = isset( $values['post_key'] ) ? $values['post_key'] : md5( mt_rand() );
parent::processBeforeCreate( $values );
}
@@ -1281,6 +1312,11 @@ class _Event extends \IPS\Content\Item implements
$values['event_dates']['start_time'] = str_replace( '.', ':', $values['event_dates']['start_time'] );
}
if( isset( $values['event_dates']['no_end_time'] ) AND $values['event_dates']['no_end_time'] AND isset( $values['event_dates']['end_time'] ) )
{
unset( $values['event_dates']['end_time'] );
}
if( isset( $values['event_dates']['end_time'] ) )
{
$values['event_dates']['end_time'] = str_replace( '.', ':', $values['event_dates']['end_time'] );
@@ -1324,7 +1360,7 @@ class _Event extends \IPS\Content\Item implements
$this->end_date = \IPS\calendar\Date::createFromForm( $values['event_dates']['start_date'], $values['event_dates']['end_time'], ( isset( $values['event_dates']['all_day'] ) AND $values['event_dates']['all_day'] ) ? 'UTC' : $values['event_dates']['event_timezone'] )->format( 'Y-m-d H:i' );
}
}
else if( ( !isset( $values['event_dates']['all_day'] ) OR !$values['event_dates']['all_day'] ) AND $values['event_dates']['end_time'] != $values['event_dates']['start_time'] )
else if( ( !isset( $values['event_dates']['all_day'] ) OR !$values['event_dates']['all_day'] ) AND isset( $values['event_dates']['end_time'] ) AND $values['event_dates']['end_time'] != $values['event_dates']['start_time'] )
{
$this->end_date = \IPS\calendar\Date::createFromForm( $values['event_dates']['start_date'], $values['event_dates']['end_time'], ( isset( $values['event_dates']['all_day'] ) AND $values['event_dates']['all_day'] ) ? 'UTC' : $values['event_dates']['event_timezone'] )->format( 'Y-m-d H:i' );
}
@@ -1365,7 +1401,10 @@ class _Event extends \IPS\Content\Item implements
$venue = new \IPS\calendar\Venue;
$venue->address = json_encode( $values['event_location'] );
$venue->save();
/* Node titles can contain HTML, but we should prevent this from user submissions as these go right into the language DB */
$values['venue_title'] = array_map( 'strip_tags', $values['venue_title'] );
\IPS\Lang::saveCustom( 'calendar', 'calendar_venue_' . $venue->id, $values['venue_title'] );
\IPS\Lang::saveCustom( 'calendar', 'calendar_venue_' . $venue->id . '_desc', $values['venue_description'] );
$venue->title_seo = \IPS\Http\Url\Friendly::seoTitle( $values['venue_title'][ \IPS\Lang::defaultLanguage() ] );
@@ -1416,8 +1455,6 @@ class _Event extends \IPS\Content\Item implements
{
parent::processAfterCreate( $comment, $values );
$this->expireWidgetCaches();
/* And claim attachments */
\IPS\File::claimAttachments( 'calendar-event', $this->id );
@@ -1671,7 +1708,7 @@ class _Event extends \IPS\Content\Item implements
foreach( $recurringEvents as $event )
{
/* Find occurrences within our date range (if any) */
$thisEndDate = ( $endDate ? $endDate : $startDate->adjust( "+2 years" ) );
$thisEndDate = ( $endDate ? $endDate->setTime( 23, 59, 59 ) : $startDate->adjust( "+2 years" ) );
$occurrences = $event->findOccurrences( $startDate, $thisEndDate );
/* Do we have any? */
@@ -1739,6 +1776,36 @@ class _Event extends \IPS\Content\Item implements
$formattedEvents = array_slice( $formattedEvents, 0, $limit, TRUE );
}
}
/* Resort formatted events by time */
else
{
foreach( $formattedEvents as $date => $type )
{
foreach( $type as $typeKey => $event )
{
/* @note: Error suppressor is needed due to PHP bug https://bugs.php.net/bug.php?id=50688 */
@usort( $formattedEvents[ $date ][ $typeKey ], function( $a, $b ) use ( $startDate )
{
if( $a->nextOccurrence( $startDate, 'startDate' ) === NULL )
{
return -1;
}
if( $b->nextOccurrence( $startDate, 'startDate' ) === NULL )
{
return 1;
}
if ( $a->nextOccurrence( $startDate, 'startDate' )->mysqlDatetime() == $b->nextOccurrence( $startDate, 'startDate' )->mysqlDatetime() )
{
return 0;
}
return ( $a->nextOccurrence( $startDate, 'startDate' )->format( 'H:i:s' ) < $b->nextOccurrence( $startDate, 'startDate' )->format( 'H:i:s' ) ) ? -1 : 1;
} );
}
}
}
return $formattedEvents;
}
@@ -1855,6 +1922,7 @@ class _Event extends \IPS\Content\Item implements
/**
* Get output for API
*
* @param \IPS\Member|NULL $authorizedMember The member making the API request or NULL for API Key / client_credentials
* @return array
* @apiresponse int id ID number
* @apiresponse string title Title
@@ -1865,7 +1933,8 @@ class _Event extends \IPS\Content\Item implements
* @apiresponse bool rsvp If this event accepts RSVPs
* @apiresponse int rsvpLimit The number of RSVPs the event is limited to
* @apiresponse \IPS\GeoLocation location The location where the event is taking place
* @apiresponse \IPS\Member author The member thgat created the event
* @apiresponse \IPS\calendar\Venue venue The venue data
* @apiresponse \IPS\Member author The member that created the event
* @apiresponse datetime postedDate When the event was created
* @apiresponse string description Event description
* @apiresponse int comments Number of comments
@@ -1878,19 +1947,20 @@ class _Event extends \IPS\Content\Item implements
* @apiresponse bool featured Event is featured
* @apiresponse string url URL
*/
public function apiOutput()
public function apiOutput( \IPS\Member $authorizedMember = NULL )
{
return array(
'id' => $this->tid,
'id' => $this->id,
'title' => $this->title,
'calendar' => $this->container()->apiOutput(),
'calendar' => $this->container()->apiOutput( $authorizedMember ),
'start' => $this->_start_date->rfc3339(),
'end' => $this->_end_date ? $this->_end_date->rfc3339() : NULL,
'recurrence' => $this->recurring,
'rsvp' => (bool) $this->rsvp,
'rsvpLimit' => $this->rsvp_limit == -1 ? null : $this->rsvp_limit,
'location' => $this->location ? \IPS\GeoLocation::buildFromJson( $this->location )->apiOutput() : null,
'author' => $this->author()->apiOutput(),
'rsvpLimit' => $this->rsvp_limit == -1 ? NULL : $this->rsvp_limit,
'location' => $this->_location ? $this->_location->apiOutput( $authorizedMember ) : NULL,
'venue' => $this->venue() ? $this->venue()->apiOutput( $authorizedMember ) : NULL,
'author' => $this->author()->apiOutput( $authorizedMember ),
'postedDate' => \IPS\DateTime::ts( $this->saved )->rfc3339(),
'description' => $this->content(),
'comments' => $this->comments,
@@ -2009,4 +2079,30 @@ class _Event extends \IPS\Content\Item implements
{
return array( 'core_FeaturedComments', 'core_ContentMessages' );
}
/**
* Get the real location, this method takes also the venue into account
*
* @return NULL|\IPS\GeoLocation
*/
public function get__location()
{
if ( $this->locationData === NULL )
{
if ( $this->venue() )
{
$this->locationData = \IPS\GeoLocation::buildFromjson( $this->venue()->address );
}
else if ( $this->location )
{
$this->locationData = \IPS\GeoLocation::buildFromjson( $this->location );
}
else
{
$this->locationData = FALSE;
}
}
return $this->locationData;
}
}
@@ -21,9 +21,9 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
/**
* Event Review Model
*/
class _Review extends \IPS\Content\Review implements \IPS\Content\EditHistory, \IPS\Content\ReportCenter, \IPS\Content\Hideable, \IPS\Content\Searchable, \IPS\Content\Embeddable
class _Review extends \IPS\Content\Review implements \IPS\Content\EditHistory, \IPS\Content\Hideable, \IPS\Content\Searchable, \IPS\Content\Embeddable
{
use \IPS\Content\Reactable;
use \IPS\Content\Reactable, \IPS\Content\Reportable;
/**
* @brief [ActiveRecord] Multiton Store
@@ -63,6 +63,7 @@ class _Review extends \IPS\Content\Review implements \IPS\Content\EditHistory, \
'votes_helpful' => 'votes_helpful',
'votes_data' => 'votes_data',
'approved' => 'approved',
'author_response' => 'author_response',
);
/**
@@ -103,7 +104,7 @@ class _Review extends \IPS\Content\Review implements \IPS\Content\EditHistory, \
*/
public static function contentTableTemplate()
{
return array( \IPS\Theme::i()->getTemplate( 'tables', 'core' ), 'commentRows' );
return array( \IPS\Theme::i()->getTemplate( 'tables', 'core', 'front' ), 'commentRows' );
}
/**
@@ -303,15 +303,30 @@ class _ICSParser
$_imported = 0;
$_skipped = 0;
$_newEvents = 0;
$maxImport = 500;
// Leave this here - useful for debugging
// print_r($this->_parsedIcsData);exit;
if ( count( $this->_parsedIcsData ) and isset( $this->_parsedIcsData['events'] ) )
{
usort( $this->_parsedIcsData['events'], function( $a, $b ) {
if ( $a['start']['raw_ts'] == $b['start']['raw_ts'] )
{
return 0;
}
return ( $a['start']['raw_ts'] > $b['start']['raw_ts'] ) ? -1 : 1;
} );
/* Loop over the events */
foreach( $this->_parsedIcsData['events'] as $event )
{
if ( $_imported > $maxImport )
{
break;
}
/* Quickly, if we don't support the recurrence data provided let's just skip this event */
if( isset( $event['recurr'] ) )
{
@@ -413,7 +428,7 @@ class _ICSParser
$_new = TRUE;
/* Basics */
$newEvent->post_key = md5( uniqid() );
$newEvent->post_key = md5( mt_rand() );
$newEvent->member_id = $this->member->member_id;
$newEvent->calendar_id = $this->calendar->id;
@@ -1312,7 +1327,20 @@ class _ICSParser
/* Is a timezone specified? */
if ( isset( $event[ $method ]['tzid'] ) AND $event[ $method ]['tzid'] )
{
$event[ $method ]['utc_time'] = new \IPS\DateTime( $event[ $method ]['raw'], new \DateTimeZone( $event[ $method ]['tzid'] ) );
/* Let's test it first... */
if( in_array( $event[ $method ]['tzid'], \IPS\DateTime::getTimezoneIdentifiers() ) )
{
try
{
$timezone = new \DateTimeZone( $event[ $method ]['tzid'] );
}
catch ( \Exception $e )
{
$timezone = NULL;
}
}
$event[ $method ]['utc_time'] = new \IPS\DateTime( $event[ $method ]['raw'], $timezone );
}
elseif( isset( $event[ $method ]['raw'] ) )
{
+20 -2
View File
@@ -142,7 +142,7 @@ class _Venue extends \IPS\Node\Model
{
$form->add( new \IPS\Helpers\Form\Translatable( 'venue_title', NULL, TRUE, array( 'app' => 'calendar', 'key' => ( $this->id ? "calendar_venue_{$this->id}" : NULL ) ) ) );
$form->add( new \IPS\Helpers\Form\Translatable( 'venue_description', NULL, FALSE, array( 'app' => 'calendar', 'key' => ( $this->id ? "calendar_venue_{$this->id}_desc" : NULL ), 'editor' => array( 'app' => 'calendar', 'key' => 'Venue', 'autoSaveKey' => ( $this->id ? "calendar-venue-{$this->id}" : "calendar-new-venue" ), 'attachIds' => $this->id ? array( $this->id, NULL, 'description' ) : NULL ) ) ) );
$form->add( new \IPS\Helpers\Form\Address( 'venue_address', $this->id ? \IPS\GeoLocation::buildFromJson( $this->address ) : NULL, TRUE, array( 'minimize' => FALSE, 'requireFullAddress' => TRUE ), NULL, NULL, NULL, 'venue_address' ) );
$form->add( new \IPS\Helpers\Form\Address( 'venue_address', $this->id ? \IPS\GeoLocation::buildFromJson( $this->address ) : NULL, TRUE, array( 'minimize' => FALSE, 'requireFullAddress' => FALSE ), NULL, NULL, NULL, 'venue_address' ) );
}
/**
@@ -169,7 +169,7 @@ class _Venue extends \IPS\Node\Model
if( isset( $values['venue_description'] ) )
{
\IPS\Lang::saveCustom( 'venues', 'calendar_venue_' . $this->id .'_desc', $values['venue_description'] );
\IPS\Lang::saveCustom( 'calendar', 'calendar_venue_' . $this->id .'_desc', $values['venue_description'] );
unset( $values['venue_description'] );
}
@@ -216,4 +216,22 @@ class _Venue extends \IPS\Node\Model
return '';
}
/**
* Get output for API
*
* @param \IPS\Member|NULL $authorizedMember The member making the API request or NULL for API Key / client_credentials
* @return array
* @apiresponse int id ID number
* @apiresponse string title Title
* @apiresponse \IPS\GeoLocation address The address
*/
public function apiOutput( \IPS\Member $authorizedMember = NULL )
{
return array(
'id' => $this->id,
'title' => $this->_title,
'address' => \IPS\GeoLocation::buildFromJson( $this->address )->apiOutput( $authorizedMember )
);
}
}
+5 -3
View File
@@ -25,6 +25,7 @@ spl_autoload_register( function( $class )
$titleField = $databases[ $databaseId ]->field_title;
$contentField = $databases[ $databaseId ]->field_content;
$contentType = $databases[ $databaseId ]->key;
$titleLang = 'content_db_lang_su_' . $databaseId;
$includeInSearch = $databases[ $databaseId ]->search ? "TRUE" : "FALSE";
@@ -42,6 +43,7 @@ spl_autoload_register( function( $class )
public static \$title = '{$titleLang}';
public static \$module = 'records{$databaseId}';
public static \$includeInSearch = {$includeInSearch};
public static \$contentType = '{$contentType}';
public static \$databaseColumnMap = array(
'author' => 'member_id',
'container' => 'category_id',
@@ -185,7 +187,7 @@ EOF;
if ( mb_substr( $class, 0, 47 ) === 'IPS\cms\extensions\core\EditorLocations\Records' and is_numeric( mb_substr( $class, 47, 1 ) ) )
{
$databaseId = intval( mb_substr( $class, 47 ) );
eval( "namespace IPS\\cms\\extensions\\core\\EditorLocations; class Records{$databaseId} extends \\IPS\\cms\\extensions\\core\\EditorLocations\\Records { public static \$customDatabaseId = $databaseId; }" );
eval( "namespace IPS\\cms\\extensions\\core\\EditorLocations; class Records{$databaseId} extends \\IPS\\cms\\extensions\\core\\EditorLocations\\Records { public static \$customDatabaseId = $databaseId; public static \$buttonLocation = TRUE; }" );
}
} );
@@ -467,7 +469,7 @@ EOF;
$defaultWidgets[] = array(
'app' => 'cms',
'key' => 'Wysiwyg',
'unique' => uniqid(),
'unique' => mt_rand(),
'configuration' => array( 'content' => $defaultContent )
);
@@ -475,7 +477,7 @@ EOF;
$defaultWidgets[] = array(
'app' => 'cms',
'key' => 'Database',
'unique' => uniqid(),
'unique' => mt_rand(),
'configuration' => array( 'database' => $database->id )
);
+82 -21
View File
@@ -68,6 +68,7 @@ class _comments extends \IPS\Content\Api\CommentController
* GET /cms/comments/{database_id}
* Get list of comments
*
* @note For requests using an OAuth Access Token for a particular member, only comments the authorized user can view will be included
* @param int $database Database ID
* @apiparam string categories Comma-delimited list of category IDs
* @apiparam string authors Comma-delimited list of member IDs - if provided, only topics started by those members are returned
@@ -77,7 +78,8 @@ class _comments extends \IPS\Content\Api\CommentController
* @apiparam string sortBy What to sort by. Can be 'date', 'title' or leave unspecified for ID
* @apiparam string sortDir Sort direction. Can be 'asc' or 'desc' - defaults to 'asc'
* @apiparam int page Page number
* @throws 2T311/1 INVALID_DATABASE The database ID does not exist
* @apiparam int perPage Number of results per page - defaults to 25
* @throws 2T311/1 INVALID_DATABASE The database ID does not exist or the authorized user does not have permission to view it
* @return \IPS\Api\PaginatedResponse<IPS\cms\Records\Comment>
*/
public function GETindex( $database )
@@ -86,6 +88,10 @@ class _comments extends \IPS\Content\Api\CommentController
try
{
$database = \IPS\cms\Databases::load( $database );
if ( $this->member and !$database->can( 'view', $this->member ) )
{
throw new \OutOfRangeException;
}
$this->class = 'IPS\cms\Records\Comment' . $database->id;
}
catch ( \OutOfRangeException $e )
@@ -103,8 +109,8 @@ class _comments extends \IPS\Content\Api\CommentController
*
* @param int $database Database ID
* @param int $comment Comment ID
* @throws 2T311/1 INVALID_DATABASE The database ID does not exist
* @throws 2T311/3 INVALID_ID The comment ID does not exist
* @throws 2T311/1 INVALID_DATABASE The database ID does not exist or the authorized user does not have permission to view it
* @throws 2T311/3 INVALID_ID The comment ID does not exist or the authorized user does not have permission to view it
* @return \IPS\cms\Records\Comment
*/
public function GETitem( $database, $comment )
@@ -113,6 +119,10 @@ class _comments extends \IPS\Content\Api\CommentController
try
{
$database = \IPS\cms\Databases::load( $database );
if ( $this->member and !$database->can( 'view', $this->member ) )
{
throw new \OutOfRangeException;
}
}
catch ( \OutOfRangeException $e )
{
@@ -123,7 +133,16 @@ class _comments extends \IPS\Content\Api\CommentController
try
{
$class = 'IPS\cms\Records\Comment' . $database->id;
return new \IPS\Api\Response( 200, $class::load( $comment )->apiOutput() );
if ( $this->member )
{
$object = $class::loadAndCheckPerms( $comment, $this->member );
}
else
{
$object = $class::load( $comment );
}
return new \IPS\Api\Response( 200, $object->apiOutput( $this->member ) );
}
catch ( \OutOfRangeException $e )
{
@@ -135,18 +154,20 @@ class _comments extends \IPS\Content\Api\CommentController
* POST /cms/comments/{database_id}
* Create a comment
*
* @note For requests using an OAuth Access Token for a particular member, any parameters the user doesn't have permission to use are ignored (for example, hidden will only be honoured if the authentictaed user has permission to hide content).
* @param int $database Database ID
* @reqapiparam int record The ID number of the record the comment is for
* @reqapiparam int author The ID number of the member making the comment (0 for guest)
* @reqapiparam int author The ID number of the member making the comment (0 for guest). Required for requests made using an API Key or the Client Credentials Grant Type. For requests using an OAuth Access Token for a particular member, that member will always be the author
* @apiparam string author_name If author is 0, the guest name that should be used
* @reqapiparam string content The comment content as HTML (e.g. "<p>This is a comment.</p>")
* @apiparam datetime date The date/time that should be used for the comment date. If not provided, will use the current date/time
* @apiparam string ip_address The IP address that should be stored for the comment. If not provided, will use the IP address from the API request
* @reqapiparam string content The comment content as HTML (e.g. "<p>This is a comment.</p>"). Will be sanatized for requests using an OAuth Access Token for a particular member; will be saved unaltered for requests made using an API Key or the Client Credentials Grant Type.
* @apiparam datetime date The date/time that should be used for the comment date. If not provided, will use the current date/time. Ignored for requests using an OAuth Access Token for a particular member
* @apiparam string ip_address The IP address that should be stored for the comment. If not provided, will use the IP address from the API request. Ignored for requests using an OAuth Access Token for a particular member
* @apiparam int hidden 0 = unhidden; 1 = hidden, pending moderator approval; -1 = hidden (as if hidden by a moderator)
* @throws 2T311/4 INVALID_DATABASE The database ID does not exist
* @throws 2T311/4 INVALID_DATABASE The database ID does not exist or the authorized user does not have permission to view it
* @throws 2T311/5 INVALID_ID The comment ID does not exist
* @throws 1T311/6 NO_AUTHOR The author ID does not exist
* @throws 1T311/7 NO_CONTENT No content was supplied
* @throws 2T311/D NO_PERMISSION The authorized user does not have permission to comment on that record
* @return \IPS\cms\Records\Comment
*/
public function POSTindex( $database )
@@ -155,6 +176,10 @@ class _comments extends \IPS\Content\Api\CommentController
try
{
$database = \IPS\cms\Databases::load( $database );
if ( $this->member and !$database->can( 'view', $this->member ) )
{
throw new \OutOfRangeException;
}
$this->class = 'IPS\cms\Records\Comment' . $database->id;
}
catch ( \OutOfRangeException $e )
@@ -174,18 +199,29 @@ class _comments extends \IPS\Content\Api\CommentController
}
/* Get author */
if ( \IPS\Request::i()->author )
if ( $this->member )
{
$author = \IPS\Member::load( \IPS\Request::i()->author );
if ( !$author->member_id )
if ( !$record->canComment( $this->member ) )
{
throw new \IPS\Api\Exception( 'NO_AUTHOR', '1T311/6', 404 );
throw new \IPS\Api\Exception( 'NO_PERMISSION', '2T311/D', 403 );
}
$author = $this->member;
}
else
{
$author = new \IPS\Member;
$author->name = \IPS\Request::i()->author_name;
if ( \IPS\Request::i()->author )
{
$author = \IPS\Member::load( \IPS\Request::i()->author );
if ( !$author->member_id )
{
throw new \IPS\Api\Exception( 'NO_AUTHOR', '1T311/6', 404 );
}
}
else
{
$author = new \IPS\Member;
$author->name = \IPS\Request::i()->author_name;
}
}
/* Check we have a post */
@@ -202,15 +238,17 @@ class _comments extends \IPS\Content\Api\CommentController
* POST /cms/comments/{database_id}/{comment_id}
* Edit a comment
*
* @note For requests using an OAuth Access Token for a particular member, any parameters the user doesn't have permission to use are ignored (for example, hidden will only be honoured if the authentictaed user has permission to hide content).
* @param int $database Database ID
* @param int $comment Comment ID
* @apiparam int author The ID number of the member making the comment (0 for guest)
* @apiparam int author The ID number of the member making the comment (0 for guest). Ignored for requests using an OAuth Access Token for a particular member.
* @apiparam string author_name If author is 0, the guest name that should be used
* @apiparam string content The comment content as HTML (e.g. "<p>This is a comment.</p>")
* @apiparam string content The comment content as HTML (e.g. "<p>This is a comment.</p>"). Will be sanatized for requests using an OAuth Access Token for a particular member; will be saved unaltered for requests made using an API Key or the Client Credentials Grant Type.
* @apiparam int hidden 1/0 indicating if the topic should be hidden
* @throws 2T311/7 INVALID_DATABASE The database ID does not exist
* @throws 2T311/8 INVALID_ID The comment ID does not exist
* @throws 2T311/7 INVALID_DATABASE The database ID does not exist or the authorized user does not have permission to view it
* @throws 2T311/8 INVALID_ID The comment ID does not exist or the authorized user does not have permission to view it
* @throws 1T311/9 NO_AUTHOR The author ID does not exist
* @throws 2T311/E NO_PERMISSION The authorized user does not have permission to edit the comment
* @return \IPS\cms\Records\Comment
*/
public function POSTitem( $database, $comment )
@@ -219,6 +257,10 @@ class _comments extends \IPS\Content\Api\CommentController
try
{
$database = \IPS\cms\Databases::load( $database );
if ( $this->member and !$database->can( 'view', $this->member ) )
{
throw new \OutOfRangeException;
}
$this->class = 'IPS\cms\Records\Comment' . $database->id;
}
catch ( \OutOfRangeException $e )
@@ -231,6 +273,14 @@ class _comments extends \IPS\Content\Api\CommentController
{
/* Load */
$comment = call_user_func( array( $this->class, 'load' ), $comment );
if ( $this->member and !$comment->canView( $this->member ) )
{
throw new \OutOfRangeException;
}
if ( $this->member and !$comment->canEdit( $this->member ) )
{
throw new \IPS\Api\Exception( 'NO_PERMISSION', '2T311/E', 403 );
}
/* Do it */
try
@@ -254,8 +304,9 @@ class _comments extends \IPS\Content\Api\CommentController
*
* @param int $database Database ID
* @param int $comment Comment ID
* @throws 2T311/A INVALID_DATABASE The database ID does not exist
* @throws 2T311/A INVALID_DATABASE The database ID does not exist or the authorized user does not have permission to view it
* @throws 2T311/B INVALID_ID The comment ID does not exist
* @throws 2T311/F NO_PERMISSION The authorized user does not have permission to delete the comment
* @return void
*/
public function DELETEitem( $database, $comment )
@@ -264,6 +315,10 @@ class _comments extends \IPS\Content\Api\CommentController
try
{
$database = \IPS\cms\Databases::load( $database );
if ( $this->member and !$database->can( 'view', $this->member ) )
{
throw new \OutOfRangeException;
}
$this->class = 'IPS\cms\Records\Comment' . $database->id;
}
catch ( \OutOfRangeException $e )
@@ -274,7 +329,13 @@ class _comments extends \IPS\Content\Api\CommentController
/* Do it */
try
{
call_user_func( array( $this->class, 'load' ), $comment )->delete();
$class = $this->class;
$object = $class::load( $comment );
if ( $this->member and !$object->canDelete( $this->member ) )
{
throw new \IPS\Api\Exception( 'NO_PERMISSION', '2T311/F', 403 );
}
$object->delete();
return new \IPS\Api\Response( 200, NULL );
}
+150 -40
View File
@@ -68,6 +68,7 @@ class _records extends \IPS\Content\Api\ItemController
* GET /cms/records/{database_id}
* Get list of records
*
* @note For requests using an OAuth Access Token for a particular member, only records the authorized user can view will be included
* @param int $database Database ID
* @apiparam string categories Comma-delimited list of category IDs
* @apiparam string authors Comma-delimited list of member IDs - if provided, only records started by those members are returned
@@ -78,7 +79,8 @@ class _records extends \IPS\Content\Api\ItemController
* @apiparam string sortBy What to sort by. Can be 'date' for creation date, 'title' or leave unspecified for ID
* @apiparam string sortDir Sort direction. Can be 'asc' or 'desc' - defaults to 'asc'
* @apiparam int page Page number
* @throws 2T306/1 INVALID_DATABASE The database ID does not exist
* @apiparam int perPage Number of results per page - defaults to 25
* @throws 2T306/1 INVALID_DATABASE The database ID does not exist or the authorized user does not have permission to view it
* @return \IPS\Api\PaginatedResponse<IPS\cms\Records>
*/
public function GETindex( $database )
@@ -87,6 +89,10 @@ class _records extends \IPS\Content\Api\ItemController
try
{
$database = \IPS\cms\Databases::load( $database );
if ( $this->member and !$database->can( 'view', $this->member ) )
{
throw new \OutOfRangeException;
}
$this->class = 'IPS\cms\Records' . $database->id;
}
catch ( \OutOfRangeException $e )
@@ -107,8 +113,8 @@ class _records extends \IPS\Content\Api\ItemController
*
* @param int $database Database ID Number
* @param int $record Record ID Number
* @throws 2T306/2 INVALID_DATABASE The database ID does not exist
* @throws 2T306/3 INVALID_ID The record ID does not exist
* @throws 2T306/2 INVALID_DATABASE The database ID does not exist or the authorized user does not have permission to view it
* @throws 2T306/3 INVALID_ID The record ID does not exist or the authorized user does not have permission to view it
* @return \IPS\cms\Records
*/
public function GETitem( $database, $record )
@@ -117,6 +123,11 @@ class _records extends \IPS\Content\Api\ItemController
try
{
$database = \IPS\cms\Databases::load( $database );
if ( $this->member and !$database->can( 'view', $this->member ) )
{
throw new \OutOfRangeException;
}
$this->class = 'IPS\cms\Records' . $database->id;
}
catch ( \OutOfRangeException $e )
@@ -127,7 +138,13 @@ class _records extends \IPS\Content\Api\ItemController
/* Return */
try
{
return new \IPS\Api\Response( 200, call_user_func( array( $this->class, 'load' ), $record )->apiOutput() );
$record = call_user_func( array( $this->class, 'load' ), $record );
if ( $this->member and !$record->can( 'read', $this->member ) )
{
throw new \OutOfRangeException;
}
return new \IPS\Api\Response( 200, $record->apiOutput( $this->member ) );
}
catch ( \OutOfRangeException $e )
{
@@ -139,14 +156,15 @@ class _records extends \IPS\Content\Api\ItemController
* POST /cms/records/{database_id}
* Create a record
*
* @note For requests using an OAuth Access Token for a particular member, any parameters the user doesn't have permission to use are ignored (for example, locked will only be honoured if the authentictaed user has permission to lock records).
* @param int $database Database ID Number
* @reqapiparam int category The ID number of the category the record should be created in. If the database does not use categories, this is not required
* @reqapiparam int author The ID number of the member creating the record (0 for guest)
* @reqapiparam object fields Field values. Keys should be the field ID, and the value should be the value
* @reqapiparam int author The ID number of the member creating the record (0 for guest) Required for requests made using an API Key or the Client Credentials Grant Type. For requests using an OAuth Access Token for a particular member, that member will always be the author
* @reqapiparam object fields Field values. Keys should be the field ID, and the value should be the value. For requests using an OAuth Access Token for a particular member, values will be sanatised where necessary. For requests made using an API Key or the Client Credentials Grant Type values will be saved unchanged.
* @apiparam string prefix Prefix tag
* @apiparam string tags Comma-separated list of tags (do not include prefix)
* @apiparam datetime date The date/time that should be used for the record date. If not provided, will use the current date/time
* @apiparam string ip_address The IP address that should be stored for the record. If not provided, will use the IP address from the API request
* @apiparam datetime date The date/time that should be used for the record date. If not provided, will use the current date/time. Ignored for requests using an OAuth Access Token for a particular member.
* @apiparam string ip_address The IP address that should be stored for the record. If not provided, will use the IP address from the API request. Ignored for requests using an OAuth Access Token for a particular member.
* @apiparam int locked 1/0 indicating if the record should be locked
* @apiparam int hidden 0 = unhidden; 1 = hidden, pending moderator approval; -1 = hidden (as if hidden by a moderator)
* @apiparam int pinned 1/0 indicating if the record should be pinned
@@ -154,6 +172,7 @@ class _records extends \IPS\Content\Api\ItemController
* @throws 2T306/4 INVALID_DATABASE The database ID does not exist
* @throws 1T306/5 NO_CATEGORY The category ID does not exist
* @throws 1T306/6 NO_AUTHOR The author ID does not exist
* @throws 2T306/G NO_PERMISSION The authorized user does not have permission to create a record in that category
* @return \IPS\cms\Records
*/
public function POSTindex( $database )
@@ -172,13 +191,15 @@ class _records extends \IPS\Content\Api\ItemController
/* Get category */
try
{
$categoryClass = 'IPS\cms\Categories' . $database->id;
if ( $database->use_categories )
{
$category = \IPS\cms\Categories::load( \IPS\Request::i()->category );
$category = $categoryClass::load( \IPS\Request::i()->category );
}
else
{
$category = \IPS\cms\Categories::load( $database->default_category );
$category = $categoryClass::load( $database->default_category );
}
}
catch ( \OutOfRangeException $e )
@@ -187,45 +208,71 @@ class _records extends \IPS\Content\Api\ItemController
}
/* Get author */
if ( \IPS\Request::i()->author )
if ( $this->member )
{
$author = \IPS\Member::load( \IPS\Request::i()->author );
if ( !$author->member_id )
if ( !$category->can( 'add', $this->member ) )
{
throw new \IPS\Api\Exception( 'NO_AUTHOR', '1T306/6', 400 );
throw new \IPS\Api\Exception( 'NO_PERMISSION', '2T306/G', 403 );
}
$author = $this->member;
}
else
{
$author = new \IPS\Member;
if ( \IPS\Request::i()->author )
{
$author = \IPS\Member::load( \IPS\Request::i()->author );
if ( !$author->member_id )
{
throw new \IPS\Api\Exception( 'NO_AUTHOR', '1T306/6', 400 );
}
}
else
{
$author = new \IPS\Member;
}
}
$record = $this->_create( $category, $author );
/* Sync Topic */
$class = $this->class;
if ( !$class::$skipTopicCreation and \IPS\Application::appIsEnabled('forums') and $record->_forum_record and $record->_forum_forum and ! $record->hidden() and ! $record->record_future_date )
{
try
{
$record->syncTopic();
}
catch( \Exception $ex ) { }
}
/* Do it */
return new \IPS\Api\Response( 201, $this->_create( $category, $author )->apiOutput() );
return new \IPS\Api\Response( 201, $record->apiOutput( $this->member ) );
}
/**
* POST /cms/records/{database_id}/{record_id}
* Edit a record
*
* @note For requests using an OAuth Access Token for a particular member, any parameters the user doesn't have permission to use are ignored (for example, locked will only be honoured if the authentictaed user has permission to lock topics).
* @param int $database Database ID Number
* @param int $record Record ID Number
* @param int $database Database ID Number
* @apiparam int category The ID number of the category the record should be created in. If the database does not use categories, this is not required
* @apiparam int author The ID number of the member creating the record (0 for guest)
* @apiparam object fields Field values. Keys should be the field ID, and the value should be the value
* @apiparam int author The ID number of the member creating the record (0 for guest). Ignored for requests using an OAuth Access Token for a particular member.
* @reqapiparam object fields Field values. Keys should be the field ID, and the value should be the value. For requests using an OAuth Access Token for a particular member, values will be sanatised where necessary. For requests made using an API Key or the Client Credentials Grant Type values will be saved unchanged.
* @apiparam string prefix Prefix tag
* @apiparam string tags Comma-separated list of tags (do not include prefix)
* @apiparam datetime date The date/time that should be used for the record date. If not provided, will use the current date/time
* @apiparam string ip_address The IP address that should be stored for the record. If not provided, will use the IP address from the API request
* @apiparam datetime date The date/time that should be used for the record date. If not provided, will use the current date/time. Ignored for requests using an OAuth Access Token for a particular member.
* @apiparam string ip_address The IP address that should be stored for the record. If not provided, will use the IP address from the API request. Ignored for requests using an OAuth Access Token for a particular member.
* @apiparam int locked 1/0 indicating if the record should be locked
* @apiparam int hidden 0 = unhidden; 1 = hidden, pending moderator approval; -1 = hidden (as if hidden by a moderator)
* @apiparam int pinned 1/0 indicating if the record should be pinned
* @apiparam int featured 1/0 indicating if the record should be featured
* @throws 2T306/9 INVALID_DATABASE The database ID does not exist
* @throws 2T306/6 INVALID_ID The record ID is invalid
* @throws 1T306/7 NO_CATEGORY The category ID does not exist
* @throws 2T306/6 INVALID_ID The record ID is invalid or the authorized user does not have permission to view it
* @throws 1T306/7 NO_CATEGORY The category ID does not exist or the authorized user does not have permission to post in it
* @throws 1T306/8 NO_AUTHOR The author ID does not exist
* @throws 2T306/H NO_PERMISSION The authorized user does not have permission to edit the record
* @return \IPS\cms\Records
*/
public function POSTitem( $database, $record )
@@ -245,18 +292,33 @@ class _records extends \IPS\Content\Api\ItemController
try
{
$record = call_user_func( array( $this->class, 'load' ), $record );
if ( $this->member and !$record->can( 'read', $this->member ) )
{
throw new \OutOfRangeException;
}
}
catch ( \OutOfRangeException $e )
{
throw new \IPS\Api\Exception( 'INVALID_ID', '2T306/6', 404 );
}
if ( $this->member and !$record->canEdit( $this->member ) )
{
throw new \IPS\Api\Exception( 'NO_PERMISSION', '2T306/H', 403 );
}
/* New category */
if ( $database->use_categories and isset( \IPS\Request::i()->category ) and \IPS\Request::i()->category != $record->category_id )
if ( $database->use_categories and isset( \IPS\Request::i()->category ) and \IPS\Request::i()->category != $record->category_id and ( !$this->member or $record->canMove( $this->member ) ) )
{
try
{
$newCategory = \IPS\blog\Blog::load( \IPS\Request::i()->blog );
$categoryClass = 'IPS\cms\Categories' . $database->id;
$newCategory = $categoryClass::load( \IPS\Request::i()->category );
if ( $this->member and !$newCategory->can( 'add', $this->member ) )
{
throw new \OutOfRangeException;
}
$record->move( $newCategory );
}
catch ( \OutOfRangeException $e )
@@ -266,7 +328,7 @@ class _records extends \IPS\Content\Api\ItemController
}
/* New author */
if ( isset( \IPS\Request::i()->author ) )
if ( !$this->member and isset( \IPS\Request::i()->author ) )
{
try
{
@@ -285,20 +347,33 @@ class _records extends \IPS\Content\Api\ItemController
}
/* Everything else */
$this->_createOrUpdate( $record );
$this->_createOrUpdate( $record, 'edit' );
/* Save and return */
$record->save();
return new \IPS\Api\Response( 200, $record->apiOutput() );
/* Sync Topic */
$class = $this->class;
if ( !$class::$skipTopicCreation and \IPS\Application::appIsEnabled('forums') and $record->_forum_record and $record->_forum_forum and ! $record->hidden() and ! $record->record_future_date )
{
try
{
$record->syncTopic();
}
catch( \Exception $ex ) { }
}
return new \IPS\Api\Response( 200, $record->apiOutput( $this->member ) );
}
/**
* Create or update record
*
* @param \IPS\Content\Item $item The item
* @param string $type add or edit
* @return \IPS\Content\Item
*/
protected function _createOrUpdate( \IPS\Content\Item $item )
protected function _createOrUpdate( \IPS\Content\Item $item, $type='add' )
{
/* Set field values */
if ( isset( \IPS\Request::i()->fields ) )
@@ -306,13 +381,26 @@ class _records extends \IPS\Content\Api\ItemController
$fieldsClass = str_replace( 'Records', 'Fields', get_class( $item ) );
foreach ( $fieldsClass::data() as $key => $field )
{
$key = "field_{$field->_id}";
$item->$key = \IPS\Request::i()->fields[ $field->id ];
if ( isset( \IPS\Request::i()->fields[ $field->id ] ) )
{
if ( !$this->member or $field->can( $type, $this->member ) )
{
$key = "field_{$field->_id}";
$value = \IPS\Request::i()->fields[ $field->id ];
if ( $field->type === 'Editor' and $this->member )
{
$value = \IPS\Text\Parser::parseStatic( $value, TRUE, NULL, $this->member, 'cms_Records' );
}
$item->$key = $value;
}
}
}
}
/* Pass up */
return parent::_createOrUpdate( $item );
return parent::_createOrUpdate( $item, $type );
}
/**
@@ -323,9 +411,10 @@ class _records extends \IPS\Content\Api\ItemController
* @apiparam int hidden If 1, only comments which are hidden are returned, if 0 only not hidden
* @apiparam string sortDir Sort direction. Can be 'asc' or 'desc' - defaults to 'asc'
* @apiparam int page Page number
* @throws 2T306/C INVALID_DATABASE The database ID does not exist
* @throws 2T306/D INVALID_ID The entry ID does not exist
* @return \IPS\Api\PaginatedResponse<IPS\calendar\Event\Comment>
* @apiparam int perPage Number of results per page - defaults to 25
* @throws 2T306/C INVALID_DATABASE The database ID does not exist or the authorized user does not have permission to view it
* @throws 2T306/D INVALID_ID The entry ID does not exist or the authorized user does not have permission to view it
* @return \IPS\Api\PaginatedResponse<IPS\cms\Records\Comment>
*/
public function GETitem_comments( $database, $record )
{
@@ -333,6 +422,11 @@ class _records extends \IPS\Content\Api\ItemController
try
{
$database = \IPS\cms\Databases::load( $database );
if ( $this->member and !$database->can( 'view', $this->member ) )
{
throw new \OutOfRangeException;
}
$this->class = 'IPS\cms\Records' . $database->id;
}
catch ( \OutOfRangeException $e )
@@ -359,9 +453,10 @@ class _records extends \IPS\Content\Api\ItemController
* @apiparam int hidden If 1, only comments which are hidden are returned, if 0 only not hidden
* @apiparam string sortDir Sort direction. Can be 'asc' or 'desc' - defaults to 'asc'
* @apiparam int page Page number
* @throws 2T306/E INVALID_DATABASE The database ID does not exist
* @throws 2T306/F INVALID_ID The entry ID does not exist
* @return \IPS\Api\PaginatedResponse<IPS\calendar\Event\Review>
* @apiparam int perPage Number of results per page - defaults to 25
* @throws 2T306/E INVALID_DATABASE The database ID does not exist or the authorized user does not have permission to view it
* @throws 2T306/F INVALID_ID The entry ID does not exist or the authorized user does not have permission to view it
* @return \IPS\Api\PaginatedResponse<IPS\cms\Records\Review>
*/
public function GETitem_reviews( $database, $record )
{
@@ -369,6 +464,11 @@ class _records extends \IPS\Content\Api\ItemController
try
{
$database = \IPS\cms\Databases::load( $database );
if ( $this->member and !$database->can( 'view', $this->member ) )
{
throw new \OutOfRangeException;
}
$this->class = 'IPS\cms\Records' . $database->id;
}
catch ( \OutOfRangeException $e )
@@ -393,8 +493,9 @@ class _records extends \IPS\Content\Api\ItemController
*
* @param int $database Database ID Number
* @param int $record Record ID Number
* @throws 2T306/A INVALID_DATABASE The database ID does not exist
* @throws 2T306/B INVALID_ID The entry ID does not exist
* @throws 2T306/A INVALID_DATABASE The database ID does not exist or the authorized user does not have permission to view it
* @throws 2T306/B INVALID_ID The entry ID does not exist
* @throws 2T306/I NO_PERMISSION The authorized user does not have permission to delete the record.
* @return void
*/
public function DELETEitem( $database, $record )
@@ -403,6 +504,11 @@ class _records extends \IPS\Content\Api\ItemController
try
{
$database = \IPS\cms\Databases::load( $database );
if ( $this->member and !$database->can( 'view', $this->member ) )
{
throw new \OutOfRangeException;
}
$this->class = 'IPS\cms\Records' . $database->id;
}
catch ( \OutOfRangeException $e )
@@ -419,6 +525,10 @@ class _records extends \IPS\Content\Api\ItemController
{
throw new \IPS\Api\Exception( 'INVALID_ID', '2T306/B', 404 );
}
if ( $this->member and !$record->canDelete( $this->member ) )
{
throw new \IPS\Api\Exception( 'NO_PERMISSION', '2T306/I', 404 );
}
/* Delete and return */
$record->delete();
+82 -21
View File
@@ -68,6 +68,7 @@ class _reviews extends \IPS\Content\Api\CommentController
* GET /cms/reviews/{database_id}
* Get list of comments
*
* @note For requests using an OAuth Access Token for a particular member, only reviews the authorized user can view will be included
* @param int $database Database ID
* @apiparam string categories Comma-delimited list of category IDs
* @apiparam string authors Comma-delimited list of member IDs - if provided, only topics started by those members are returned
@@ -77,7 +78,8 @@ class _reviews extends \IPS\Content\Api\CommentController
* @apiparam string sortBy What to sort by. Can be 'date', 'title' or leave unspecified for ID
* @apiparam string sortDir Sort direction. Can be 'asc' or 'desc' - defaults to 'asc'
* @apiparam int page Page number
* @throws 2T312/1 INVALID_DATABASE The database ID does not exist
* @apiparam int perPage Number of results per page - defaults to 25
* @throws 2T312/1 INVALID_DATABASE The database ID does not exist or the authorized user does not have permission to view it
* @return \IPS\Api\PaginatedResponse<IPS\cms\Records\Review>
*/
public function GETindex( $database )
@@ -86,6 +88,10 @@ class _reviews extends \IPS\Content\Api\CommentController
try
{
$database = \IPS\cms\Databases::load( $database );
if ( $this->member and !$database->can( 'view', $this->member ) )
{
throw new \OutOfRangeException;
}
$this->class = 'IPS\cms\Records\Review' . $database->id;
}
catch ( \OutOfRangeException $e )
@@ -103,8 +109,8 @@ class _reviews extends \IPS\Content\Api\CommentController
*
* @param int $database Database ID
* @param int $review Comment ID
* @throws 2T312/2 INVALID_DATABASE The database ID does not exist
* @throws 2T311/3 INVALID_ID The comment ID does not exist
* @throws 2T312/2 INVALID_DATABASE The database ID does not exist or the authorized user does not have permission to view it
* @throws 2T311/3 INVALID_ID The comment ID does not exist or the authorized user does not have permission to view it
* @return \IPS\cms\Records\Review
*/
public function GETitem( $database, $review )
@@ -113,6 +119,10 @@ class _reviews extends \IPS\Content\Api\CommentController
try
{
$database = \IPS\cms\Databases::load( $database );
if ( $this->member and !$database->can( 'view', $this->member ) )
{
throw new \OutOfRangeException;
}
}
catch ( \OutOfRangeException $e )
{
@@ -123,7 +133,16 @@ class _reviews extends \IPS\Content\Api\CommentController
try
{
$class = 'IPS\cms\Records\Review' . $database->id;
return new \IPS\Api\Response( 200, $class::load( $review )->apiOutput() );
if ( $this->member )
{
$object = $class::loadAndCheckPerms( $id, $this->member );
}
else
{
$object = $class::load( $id );
}
return new \IPS\Api\Response( 200, $object->apiOutput( $this->member ) );
}
catch ( \OutOfRangeException $e )
{
@@ -135,20 +154,22 @@ class _reviews extends \IPS\Content\Api\CommentController
* POST /cms/reviews/{database_id}
* Create a comment
*
* @note For requests using an OAuth Access Token for a particular member, any parameters the user doesn't have permission to use are ignored (for example, hidden will only be honoured if the authentictaed user has permission to hide content).
* @param int $database Database ID
* @reqapiparam int record The ID number of the record the comment is for
* @reqapiparam int author The ID number of the member making the comment (0 for guest)
* @reqapiparam int author The ID number of the member making the comment (0 for guest). Required for requests made using an API Key or the Client Credentials Grant Type. For requests using an OAuth Access Token for a particular member, that member will always be the author
* @apiparam string author_name If author is 0, the guest name that should be used
* @reqapiparam string content The comment content as HTML (e.g. "<p>This is a comment.</p>")
* @apiparam datetime date The date/time that should be used for the comment date. If not provided, will use the current date/time
* @apiparam string ip_address The IP address that should be stored for the comment. If not provided, will use the IP address from the API request
* @reqapiparam string content The comment content as HTML (e.g. "<p>This is a comment.</p>"). Will be sanatized for requests using an OAuth Access Token for a particular member; will be saved unaltered for requests made using an API Key or the Client Credentials Grant Type.
* @apiparam datetime date The date/time that should be used for the comment date. If not provided, will use the current date/time. Ignored for requests using an OAuth Access Token for a particular member
* @apiparam string ip_address The IP address that should be stored for the comment. If not provided, will use the IP address from the API request. Ignored for requests using an OAuth Access Token for a particular member
* @apiparam int hidden 0 = unhidden; 1 = hidden, pending moderator approval; -1 = hidden (as if hidden by a moderator)
* @reqapiparam int rating Star rating
* @throws 2T312/4 INVALID_DATABASE The database ID does not exist
* @throws 2T312/4 INVALID_DATABASE The database ID does not exist or the authorized user does not have permission to view it
* @throws 2T312/5 INVALID_ID The comment ID does not exist
* @throws 1T312/6 NO_AUTHOR The author ID does not exist
* @throws 1T312/7 NO_CONTENT No content was supplied
* @throws 1T312/8 INVALID_RATING The rating is not a valid number up to the maximum rating
* @throws 2T312/E NO_PERMISSION The authorized user does not have permission to review that record
* @return \IPS\cms\Records\Review
*/
public function POSTindex( $database )
@@ -157,6 +178,10 @@ class _reviews extends \IPS\Content\Api\CommentController
try
{
$database = \IPS\cms\Databases::load( $database );
if ( $this->member and !$database->can( 'view', $this->member ) )
{
throw new \OutOfRangeException;
}
$this->class = 'IPS\cms\Records\Review' . $database->id;
}
catch ( \OutOfRangeException $e )
@@ -176,18 +201,29 @@ class _reviews extends \IPS\Content\Api\CommentController
}
/* Get author */
if ( \IPS\Request::i()->author )
if ( $this->member )
{
$author = \IPS\Member::load( \IPS\Request::i()->author );
if ( !$author->member_id )
if ( !$record->canReview( $this->member ) )
{
throw new \IPS\Api\Exception( 'NO_AUTHOR', '1T312/6', 404 );
throw new \IPS\Api\Exception( 'NO_PERMISSION', '2T312/E', 403 );
}
$author = $this->member;
}
else
{
$author = new \IPS\Member;
$author->name = \IPS\Request::i()->author_name;
if ( \IPS\Request::i()->author )
{
$author = \IPS\Member::load( \IPS\Request::i()->author );
if ( !$author->member_id )
{
throw new \IPS\Api\Exception( 'NO_AUTHOR', '1T312/6', 404 );
}
}
else
{
$author = new \IPS\Member;
$author->name = \IPS\Request::i()->author_name;
}
}
/* Check we have a post */
@@ -210,16 +246,18 @@ class _reviews extends \IPS\Content\Api\CommentController
* POST /cms/reviews/{database_id}/{review_id}
* Edit a comment
*
* @note For requests using an OAuth Access Token for a particular member, any parameters the user doesn't have permission to use are ignored (for example, hidden will only be honoured if the authentictaed user has permission to hide content).
* @param int $database Database ID
* @param int $review Review ID
* @apiparam int author The ID number of the member making the review (0 for guest)
* @apiparam int author The ID number of the member making the review (0 for guest). Ignored for requests using an OAuth Access Token for a particular member.
* @apiparam string author_name If author is 0, the guest name that should be used
* @apiparam string content The comment content as HTML (e.g. "<p>This is a comment.</p>")
* @apiparam string content The comment content as HTML (e.g. "<p>This is a comment.</p>"). Will be sanatized for requests using an OAuth Access Token for a particular member; will be saved unaltered for requests made using an API Key or the Client Credentials Grant Type.
* @apiparam int hidden 1/0 indicating if the topic should be hidden
* @apiparam int rating Star rating
* @throws 2T312/9 INVALID_DATABASE The database ID does not exist
* @throws 2T312/A INVALID_ID The comment ID does not exist
* @throws 2T312/9 INVALID_DATABASE The database ID does not exist or the authorized user does not have permission to view it
* @throws 2T312/A INVALID_ID The comment ID does not exist or the authorized user does not have permission to view it
* @throws 1T312/B NO_AUTHOR The author ID does not exist
* @throws 2T312/F NO_PERMISSION The authorized user does not have permission to edit the review
* @return \IPS\cms\Records\Review
*/
public function POSTitem( $database, $review )
@@ -228,6 +266,10 @@ class _reviews extends \IPS\Content\Api\CommentController
try
{
$database = \IPS\cms\Databases::load( $database );
if ( $this->member and !$database->can( 'view', $this->member ) )
{
throw new \OutOfRangeException;
}
$this->class = 'IPS\cms\Records\Review' . $database->id;
}
catch ( \OutOfRangeException $e )
@@ -240,6 +282,14 @@ class _reviews extends \IPS\Content\Api\CommentController
{
/* Load */
$review = call_user_func( array( $this->class, 'load' ), $review );
if ( $this->member and !$review->canView( $this->member ) )
{
throw new \OutOfRangeException;
}
if ( $this->member and !$review->canEdit( $this->member ) )
{
throw new \IPS\Api\Exception( 'NO_PERMISSION', '2T312/F', 403 );
}
/* Do it */
try
@@ -263,8 +313,9 @@ class _reviews extends \IPS\Content\Api\CommentController
*
* @param int $database Database ID
* @param int $review Comment ID
* @throws 2T312/C INVALID_DATABASE The database ID does not exist
* @throws 2T312/C INVALID_DATABASE The database ID does not exist or the authorized user does not have permission to view it
* @throws 2T312/D INVALID_ID The comment ID does not exist
* @throws 2T312/G NO_PERMISSION The authorized user does not have permission to delete the review
* @return void
*/
public function DELETEitem( $database, $review )
@@ -273,6 +324,10 @@ class _reviews extends \IPS\Content\Api\CommentController
try
{
$database = \IPS\cms\Databases::load( $database );
if ( $this->member and !$database->can( 'view', $this->member ) )
{
throw new \OutOfRangeException;
}
$this->class = 'IPS\cms\Records\Review' . $database->id;
}
catch ( \OutOfRangeException $e )
@@ -283,7 +338,13 @@ class _reviews extends \IPS\Content\Api\CommentController
/* Do it */
try
{
call_user_func( array( $this->class, 'load' ), $review )->delete();
$class = $this->class;
$object = $class::load( $id );
if ( $this->member and !$object->canDelete( $this->member ) )
{
throw new \IPS\Api\Exception( 'NO_PERMISSION', '2T312/G', 403 );
}
$object->delete();
return new \IPS\Api\Response( 200, NULL );
}
+2 -1
View File
@@ -44,7 +44,8 @@
"categories_add": "categories_add",
"categories_edit": "categories_edit",
"categories_massManageContent": "categories_massManageContent",
"categories_delete": "categories_delete"
"categories_delete": "categories_delete",
"categories_permissions": "categories_permissions"
},
"records": {
"records_manage": "records_manage",
+72 -22
View File
@@ -456,7 +456,7 @@
{{endif}}
{{if $database->_description}}
<div class='ipsPageHeader_info ipsType_light'>
<div class='ipsPageHeader_info'>
{$database->_description}
</div>
{{endif}}
@@ -575,7 +575,7 @@
</html>]]></template>
<template template_key="database_display_comment" template_title="comment" template_desc="" template_location="database" template_group="display" template_params="$item, $comment, $editorName, $app, $type, $class=''" template_app="" template_type="template"><![CDATA[{{$itemIdField = $item::$databaseColumnId; $idField = $comment::$databaseColumnId;}}
<div id='comment-{$comment->$idField}_wrap' data-controller='core.front.core.comment' data-commentApp='{$app}' data-commentType='{$type}' data-commentID="{$comment->$idField}" data-quoteData='{expression="json_encode( array('userid' => $comment->author()->member_id, 'username' => $comment->author()->name, 'timestamp' => $comment->mapped('date'), 'contentapp' => $app, 'contenttype' => $type, 'contentclass' => $class, 'contentid' => $item->$itemIdField, 'contentcommentid' => $comment->$idField) )"}' class='cPost_content ipsComment_content ipsType_medium {{if $comment->hidden()}}ipsModerated{{endif}} ipsFaded_withHover'>
{{if \IPS\IPS::classUsesTrait( $comment, 'IPS\Content\Reactable' ) and settings.reputation_highlight and $comment->reactionCount() >= settings.reputation_highlight and settings.reputation_enabled}}
{{if settings.reputation_enabled and \IPS\IPS::classUsesTrait( $comment, 'IPS\Content\Reactable' ) and settings.reputation_highlight and $comment->reactionCount() >= settings.reputation_highlight}}
<strong class='ipsComment_popularFlag' data-ipsTooltip title='{lang="this_is_a_popular_comment"}'><i class='fa fa-heart'></i></strong>
{{endif}}
@@ -584,10 +584,14 @@
<div>
<div class='ipsPos_right ipsType_reset ipsType_light ipsType_blendLinks'>
<ul class='ipsList_inline ipsComment_tools'>
{{if $comment->canReport()}}
{{if $comment->canReportOrRevoke() === TRUE}}
<li><a href='{$comment->url('report')}' data-ipsDialog data-ipsDialog-remoteSubmit data-ipsDialog-size='medium' data-ipsDialog-flashMessage='{lang="report_submit_success"}' data-ipsDialog-title="{lang="report"}" data-action='reportComment' title='{lang="report_content"}'><span class='ipsResponsive_showPhone ipsResponsive_inline'><i class='fa fa-flag'></i></span><span class='ipsResponsive_hidePhone ipsResponsive_inline'>{lang="report_reply"}</span></a></li>
{{endif}}
{{if \IPS\Member::loggedIn()->modPermission('can_use_ip_tools') and \IPS\Member::loggedIn()->canAccessModule( \IPS\Application\Module::get( 'core', 'modcp' ) )}}
<li><span class='ipsResponsive_hidePhone ipsType_blendLinks'>(<a href="{url="app=core&module=modcp&controller=modcp&tab=ip_tools&ip=$comment->ip_address" seoTemplate="modcp_ip_tools"}">{lang="ip_prefix" sprintf="$comment->ip_address"}</a>) </span></li>
{{endif}}
<li><a class='ipsType_blendLinks' href='{$comment->item()->url()->setQueryString( array( 'do' => 'findComment', 'comment' => $comment->$idField ) )}' data-ipsTooltip title='{lang="share_this_comment"}' data-ipsMenu data-ipsMenu-closeOnClick='false' id='elShareComment_{$comment->id}' data-role='shareComment'><i class='fa fa-share-alt'></i></a></li>
{{if count( $item->commentMultimodActions() )}}
<li><span class='ipsCustomInput'>
@@ -748,17 +752,20 @@
{{endif}}
<a id='comment-{$comment->$idField}'></a>
<article {{if $comment->author()->hasHighlightedReplies()}}data-memberGroup="{$comment->author()->member_group_id}" {{endif}} id='elComment_{$comment->$idField}' class='ipsComment {{if $otherClasses}}{$otherClasses}{{endif}} {{if \IPS\IPS::classUsesTrait( $comment, 'IPS\Content\Reactable' ) and settings.reputation_highlight and $comment->reactionCount() >= settings.reputation_highlight and settings.reputation_enabled}}ipsComment_popular{{endif}} ipsComment_parent ipsClearfix ipsClear {{if $comment->author()->hasHighlightedReplies()}}ipsComment_highlighted{{endif}} {{if $comment->isIgnored()}}ipsHide{{endif}} {{if $comment->hidden() OR $item->hidden() == -2}}ipsModerated{{endif}}'>
<article {{if $comment->author()->hasHighlightedReplies()}}data-memberGroup="{$comment->author()->member_group_id}" {{endif}} id='elComment_{$comment->$idField}' class='ipsComment {{if $otherClasses}}{$otherClasses}{{endif}} {{if settings.reputation_enabled and \IPS\IPS::classUsesTrait( $comment, 'IPS\Content\Reactable' ) and settings.reputation_highlight and $comment->reactionCount() >= settings.reputation_highlight}}ipsComment_popular{{endif}} ipsComment_parent ipsClearfix ipsClear {{if $comment->author()->hasHighlightedReplies()}}ipsComment_highlighted{{endif}} {{if $comment->isIgnored()}}ipsHide{{endif}} {{if $comment->hidden() OR $item->hidden() == -2}}ipsModerated{{endif}}'>
{template="comment" group="display" location="database" app="cms" params="$item, $comment, $item::$formLangPrefix . 'comment', $item::$application, $item::$module, $itemClassSafe"}
</article>]]></template>
<template template_key="database_display_comments" template_title="comments" template_desc="" template_location="database" template_group="display" template_params="$record" template_app="" template_type="template"><![CDATA[<div data-controller='core.front.core.commentFeed, core.front.core.ignoredComments' {{if settings.auto_polling_enabled}}data-autoPoll{{endif}} data-commentsType='comments' data-baseURL='{$record->url()}' {{if $record->isLastPage()}}data-lastPage{{endif}} data-feedID='records{$record::$customDatabaseId}-{$record->_id}' id='comments'>
{template="featuredComments" group="global" app="core" params="$record->featuredComments(), $record->url()->setQueryString('tab', 'comments')->setQueryString('recommended', 'comments')"}
{template="commentMultimodHeader" app="core" group="global" params="$record, '#comments'"}
{{if $record->commentPageCount() > 1}}
{$record->commentPagination( array( 'tab' ) )|raw}
<br><br>
{{endif}}
{template="featuredComments" group="global" app="core" params="$record->featuredComments(), $record->url()->setQueryString('tab', 'comments')->setQueryString('recommended', 'comments')"}
<div data-role='commentFeed' data-controller='core.front.core.moderation'>
{{if count( $record->comments( NULL, NULL, 'date', 'asc', NULL, NULL, NULL, NULL, FALSE, isset( \IPS\Request::i()->showDeleted ) ) )}}
<form action="{$record->url()->csrf()->setQueryString( 'do', 'multimodComment' )}" method="post" data-ipsPageAction data-role='moderationTools'>
@@ -782,7 +789,7 @@
<hr class='ipsHr'>
{$record->commentPagination( array( 'tab' ) )|raw}
{{endif}}
{{if $record->commentForm() || $record->locked() || \IPS\Member::loggedin()->restrict_post || \IPS\Member::loggedIn()->members_bitoptions['unacknowledged_warnings']}}
{{if $record->commentForm() || $record->locked() || \IPS\Member::loggedin()->restrict_post || \IPS\Member::loggedIn()->members_bitoptions['unacknowledged_warnings'] || !\IPS\Member::loggedIn()->checkPostsPerDay()}}
<br><br>
<div class='ipsAreaBackground ipsPad' data-role='replyArea'>
{{if $record->commentForm()}}
@@ -797,6 +804,8 @@
{template="commentUnavailable" group="forms" location="front" app="core" params="'restricted_cannot_comment', \IPS\Member::loggedIn()->warnings(5,NULL,'rpa'), \IPS\Member::loggedIn()->restrict_post"}
{{elseif \IPS\Member::loggedIn()->members_bitoptions['unacknowledged_warnings']}}
{template="commentUnavailable" group="forms" location="front" app="core" params="'unacknowledged_warning_cannot_post', \IPS\Member::loggedIn()->warnings( 1, FALSE )"}
{{elseif !\IPS\Member::loggedIn()->checkPostsPerDay()}}
{template="commentUnavailable" group="forms" location="front" app="core" params="'member_exceeded_posts_per_day'"}
{{endif}}
{{endif}}
</div>
@@ -844,7 +853,7 @@
{template="prefix" group="global" app="core" params="$record->prefix( TRUE ), $record->prefix()"}
</span>
{{endif}}
{{if $record->canEdit()}}
{{if $record->canEditTitle()}}
<span class='ipsType_break ipsContained' data-controller="core.front.core.moderation">
<span data-role="editableTitle" title='{lang="click_hold_edit"}'>{$record->_title}</span>
</span>
@@ -864,7 +873,7 @@
<hr class='ipsHr'>
<div class='ipsPhotoPanel ipsPhotoPanel_tiny ipsClearfix ipsSpacer_bottom'>
<div class='ipsBox_alt ipsPhotoPanel ipsPhotoPanel_tiny ipsClearfix ipsSpacer_bottom'>
{template="userPhoto" app="core" group="global" params="$record->author(), 'tiny', $record->warningRef()"}
<div>
<p class='ipsType_reset ipsType_large ipsType_blendLinks'>
@@ -890,7 +899,11 @@
{template="tags" group="global" app="core" params="$record->tags(), FALSE, FALSE, ( $record->canEdit() AND $record::canTag( NULL, $record->container() ) ) ? $record->url() : NULL"}
{{endif}}
{template="contentItemMessages" group="global" app="core" params="$record->getMessages(), $record"}
{{if count($record->getMessages())}}
<div class='ipsSpacer ipsSpacer_top'>
{template="contentItemMessages" group="global" app="core" params="$record->getMessages(), $record"}
</div>
{{endif}}
<article class='ipsContained ipsSpacer_top ipsBox ipsPad'>
<div class='ipsClearfix'>
@@ -906,9 +919,9 @@
{{endif}}
{{if $records = $record->getReciprocalItems()}}
{{$infoLineShown = FALSE;}}
<br>
{{foreach $records as $fieldId => $items}}
{{$infoLineShown = FALSE;}}
<ul class="ipsList_inline ipsList_csv ipsList_noSpacing">
{{foreach $items as $item}}
<li>{{if ! $infoLineShown}}{{$infoLineShown=TRUE;}}{lang="records_linking_to_me" sprintf="$item::database()->recordWord(0,TRUE), $record::database()->recordWord(1)"}: {{endif}}<a href="{$item->url()}">{$item->_title}</a></li>
@@ -987,7 +1000,7 @@
{{if $record->canEdit()}}
<li><a href='{$record->url('edit')->csrf()}' title='{lang="edit"}' class='ipsButton ipsButton_verySmall ipsButton_link ipsButton_fullWidth'>{lang="edit"}</a></li>
{{endif}}
{{if $record->canReport()}}
{{if $record->canReportOrRevoke() === TRUE}}
<li><a href='{$record->url('report')}' class='ipsButton ipsButton_verySmall ipsButton_link ipsButton_fullWidth' data-ipsDialog data-ipsDialog-size='medium' data-ipsDialog-title="{lang="content_record_report" sprintf="$record::database()->recordWord( 1, TRUE )"}" data-ipsDialog-remoteSubmit data-ipsDialog-flashMessage="{lang="report_submit_success"}" title="{lang="content_record_report" sprintf="$record::database()->recordWord( 1, TRUE )"}">{lang="content_record_report" sprintf="$record::database()->recordWord( 1, TRUE )"}</a></li>
{{endif}}
{{if $record->canManageRevisions()}}
@@ -1053,7 +1066,7 @@
]]></template>
<template template_key="database_display_recordEditLine" template_title="recordEditLine" template_desc="" template_location="database" template_group="display" template_params="$record" template_app="" template_type="template"><![CDATA[
<p class='ipsType_reset ipsType_medium ipsType_light' data-excludequote>
<strong>{lang="date_edited" htmlsprintf="\IPS\DateTime::ts( $record->record_edit_time )->html(FALSE), htmlspecialchars( $record->record_edit_member_name, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE )"}</strong>
<strong>{lang="date_edited" htmlsprintf="\IPS\DateTime::ts( $record->record_edit_time )->html(FALSE), htmlspecialchars( $record->record_edit_member_name, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', FALSE )"}</strong>
{{if $record->record_edit_reason}}
<br>{$record->record_edit_reason}
{{endif}}
@@ -1069,7 +1082,7 @@
{template="userPhoto" app="core" group="global" params="$review->author(), 'small', $review->warningRef(), 'ipsPos_left'"}
<div>
<p class='ipsPos_right ipsType_reset'>
{{if $review->canReport()}}
{{if $review->canReportOrRevoke() === TRUE}}
<a href='{$review->url('report')}' data-ipsDialog data-ipsDialog-remoteSubmit data-ipsDialog-size='medium' data-ipsDialog-flashMessage='{lang="report_submit_success"}' data-ipsDialog-title="{lang="report"}" data-action='reportComment' title='{lang="report"}'>{lang="report"}</a> &middot;
{{endif}}
@@ -1109,8 +1122,33 @@
<div id="review-{$review->id}" data-role="commentContent" class="ipsType_richText ipsType_normal ipsContained" data-controller='core.front.core.lightboxedImages'>
{$review->content()|raw}
</div>
{{if $review->hasAuthorResponse()}}
<div class='ipsReviewResponse ipsPad ipsSpacer_bottom ipsAreaBackground_light ipsClearfix'>
<h4 class='ipsType_sectionHead ipsType_medium ipsType_bold ipsSpacer_bottom ipsType_light'>{lang="review_response_title"}</h4>
<div data-role="reviewResponse" class="ipsType_richText ipsType_normal ipsContained ipsType_light" data-controller='core.front.core.lightboxedImages'>
{$review->mapped('author_response')|raw}
</div>
{{if $review->canEditResponse() OR $review->canDeleteResponse()}}
<ul class='ipsComment_controls ipsClearfix ipsPos_right'>
{{if $review->canEditResponse()}}
<li><a class="ipsType_light" href='{$review->url('editResponse')}' data-ipsDialog data-ipsDialog-title='{lang="review_author_respond"}'>{lang="edit"}</a></li>
{{endif}}
{{if $review->canDeleteResponse()}}
<li><a class="ipsType_light" href='{$review->url('deleteResponse')}' data-confirm>{lang="delete"}</a></li>
{{endif}}
</ul>
{{endif}}
</div>
{{endif}}
{{if $review->hidden() !== 1}}
{{if $review->canRespond()}}
<div class='ipsPos_right ipsType_medium ipsSpacer_bottom ipsSpacer_half'>
<a href='{$review->url('respond')}' class='ipsButton ipsButton_verySmall ipsButton_light' data-role='respond' data-ipsDialog data-ipsDialog-title='{lang="review_author_respond"}'>{lang="review_author_respond"}</a>
</div>
{{endif}}
{{if member.member_id and ( !$review->mapped('votes_data') or !array_key_exists( member.member_id, json_decode( $review->mapped('votes_data'), TRUE ) ) ) and $review->author()->member_id != \IPS\Member::loggedIn()->member_id}}
<span class='ipsType_medium'><strong>{lang="did_you_find_this_helpful"}</strong> &nbsp;&nbsp;&nbsp;<a href='{$review->url('rate')->setQueryString( 'helpful', TRUE )->csrf()}' class='ipsButton ipsButton_verySmall ipsButton_light' data-action="rateReview"><i class='fa fa-check'></i> {lang="yes"}</a> <a href='{$review->url('rate')->setQueryString( 'helpful', FALSE )->csrf()}' class='ipsButton ipsButton_verySmall ipsButton_light' data-action="rateReview"><i class='fa fa-times'></i> {lang="no"}</a></span>
<br class='ipsClear'>
@@ -1230,6 +1268,7 @@
{{if count( $record->reviews( NULL, NULL, NULL, 'desc', NULL, NULL, NULL, NULL, isset( \IPS\Request::i()->showDeleted ) ) )}}
{{if !$record->hasReviewed()}}<hr class='ipsHr'>{{endif}}
<div class="ipsClearfix ipsSpacer_bottom">
{template="commentMultimodHeader" app="core" group="global" params="$record, '#reviews', 'review'"}
{{if $record->reviewPageCount() > 1}}
<div class="ipsPos_left ipsResponsive_noFloat">
{$record->reviewPagination( array( 'tab', 'sort' ) )|raw}
@@ -1274,14 +1313,14 @@
<template template_key="database_form_recordForm" template_title="recordForm" template_desc="" template_location="database" template_group="form" template_params="$record, $category, $database, $page, $title, $hasModOptions, $id, $action, $elements, $hiddenValues, $actionButtons, $uploadField, $class=''" template_app="" template_type="template"><![CDATA[{{$modOptions = array( 'create_record_state', 'record_expiry_date', 'record_pinned', 'record_allow_comments', 'record_comment_cutoff', 'record_meta_keywords', 'record_meta_description' );}}
{template="pageHeader" if="!\IPS\Request::i()->isAjax()" group="global" app="core" params="\IPS\Member::loggedIn()->language()->addToStack( $title )"}
<div id="elCmsPageWrap" data-pageid="{$page->id}">
<form accept-charset='utf-8' class="ipsForm {$class}" data-ipsForm="true" action="{$action}" method="post" {{if $uploadField}}enctype="multipart/form-data"{{endif}}>
<form accept-charset='utf-8' class="ipsForm {$class}" data-ipsForm data-ipsFormSubmit action="{$action}" method="post" {{if $uploadField}}enctype="multipart/form-data"{{endif}}>
<input type="hidden" name="{$id}_submitted" value="1">
{{foreach $hiddenValues as $k => $v}}
<input type="hidden" name="{$k}" value="{$v}">
{{endforeach}}
{{if $uploadField}}
<input type="hidden" name="MAX_FILE_SIZE" value="{$uploadField}">
<input type="hidden" name="plupload" value="{expression="md5( uniqid() )"}">
<input type="hidden" name="plupload" value="{expression="md5( mt_rand() )"}">
<input type="hidden" name="content_record_form_container" value="{$category->_id}">
{{endif}}
@@ -1404,7 +1443,7 @@
{lang="or"}
&nbsp;
<span class='ipsCustomInput'>
<input type="checkbox" role='checkbox' data-control="unlimited{{if count( $input->toggles )}} toggle{{endif}}" name="{$input->name}_unlimited" id='{$input->name}_unlimited' value="{$input->options['unlimited']}" {{if $input->options['unlimited'] === $input->value}}checked aria-checked='true'{{endif}} {{if $input->options['disabled']}}disabled{{endif}} {{if count( $input->toggles )}}{{if $input->toggleOn === FALSE}}data-togglesOff{{else}}data-togglesOn{{endif}}="{expression="implode( ',', $input->toggles )"}"{{endif}} aria-labelledby='label_{$input->name}_unlimited'>
<input type="checkbox" role='checkbox' data-control="unlimited{{if $input->toggles AND count( $input->toggles )}} toggle{{endif}}" name="{$input->name}_unlimited" id='{$input->name}_unlimited' value="{$input->options['unlimited']}" {{if $input->options['unlimited'] === $input->value}}checked aria-checked='true'{{endif}} {{if $input->options['disabled']}}disabled{{endif}} {{if $input->toggles AND count( $input->toggles )}}{{if $input->toggleOn === FALSE}}data-togglesOff{{else}}data-togglesOn{{endif}}="{expression="implode( ',', $input->toggles )"}"{{endif}} aria-labelledby='label_{$input->name}_unlimited'>
<span></span>
</span>
<label for='{$input->name}_unlimited' id='label_{$input->name}_unlimited' class='ipsField_unlimited'>{lang="never"}</label>
@@ -1442,7 +1481,7 @@
{{endforeach}}
{{if $uploadField}}
<input type="hidden" name="MAX_FILE_SIZE" value="{$uploadField}">
<input type="hidden" name="plupload" value="{expression="md5( uniqid() )"}">
<input type="hidden" name="plupload" value="{expression="md5( mt_rand() )"}">
<input type="hidden" name="content_record_form_container" value="{$category->_id}">
{{endif}}
@@ -1694,7 +1733,7 @@
<ul class="ipsMenu ipsMenu_auto ipsMenu_withStem ipsMenu_selectable ipsHide" id="elSortByMenu_{$table->uniqueId}_menu">
{{$custom = TRUE;}}
{{foreach $table->sortOptions as $k => $col}}
<li class="ipsMenu_item {{if $col === $table->getSortByColumn()}}{{$custom = FALSE;}}ipsMenu_itemChecked{{endif}}" data-ipsMenuValue="{$col}" data-sortDirection='{{if $col == 'title'}}asc{{else}}desc{{endif}}'><a href="{$table->baseUrl->setQueryString( array( 'filter' => $table->filter, 'sortby' => $col, 'sortdirection' => ( $col == 'title' ) ? 'asc' : 'desc', 'page' => '1' ) )}">{lang="{$table->langPrefix}sort_{$k}"}</a></li>
<li class="ipsMenu_item {{if $col === $table->getSortByColumn()}}{{$custom = FALSE;}}ipsMenu_itemChecked{{endif}}" data-ipsMenuValue="{$col}" data-sortDirection='{$table->getSortDirection( $col )}'><a href="{$table->baseUrl->setQueryString( array( 'filter' => $table->filter, 'sortby' => $col, 'sortdirection' => $table->getSortDirection( $col ), 'page' => '1' ) )}">{lang="{$table->langPrefix}sort_{$k}"}</a></li>
{{endforeach}}
{{if $table->advancedSearch}}
<li class="ipsMenu_item {{if $custom}}ipsMenu_itemChecked{{endif}}" data-noSelect="true">
@@ -1831,6 +1870,7 @@
{{$idField = $row::$databaseColumnId;}}
{{$rowIds[] = $row->$idField;}}
{{endforeach}}
{{$iposted = $table ? $table->contentPostedIn : array();}}
{{foreach $rows as $row}}
{{$idField = $row::$databaseColumnId;}}
<li class="cCmsRecord_row ipsDataItem ipsDataItem_responsivePhoto {{if $row->unread()}}ipsDataItem_unread{{endif}} {{if method_exists( $row, 'tableClass' ) && $row->tableClass()}}ipsDataItem_{$row->tableClass()}{{endif}} {{if $row->hidden()}}ipsModerated{{endif}}" data-rowID='{$row->$idField}'>
@@ -1838,10 +1878,10 @@
<div class='ipsDataItem_icon ipsPos_top'>
{{if $row->unread()}}
<a href='{$row->url( 'getNewComment' )}' title='{lang="first_unread_post"}' data-ipsTooltip>
<span class='ipsItemStatus'><i class="fa {{if in_array( $row->$idField, $table->contentPostedIn )}}fa-star{{else}}fa-circle{{endif}}"></i></span>
<span class='ipsItemStatus'><i class="fa {{if in_array( $row->$idField, $iposted )}}fa-star{{else}}fa-circle{{endif}}"></i></span>
</a>
{{else}}
{{if in_array( $row->$idField, $table->contentPostedIn )}}
{{if in_array( $row->$idField, $iposted )}}
<span class='ipsItemStatus ipsItemStatus_read ipsItemStatus_posted'><i class="fa fa-star"></i></span>
{{else}}
&nbsp;
@@ -1934,6 +1974,12 @@
{{else}}
{lang="cms_row_added_by" htmlsprintf="$row->author()->link()"}
{{endif}}
{{elseif $row->database()->_comment_bump & \IPS\cms\Databases::BUMP_ON_EDIT}}
{{if $row->record_edit_time > 0}}
{lang="cms_row_updated_by" htmlsprintf="\IPS\Member::load( $row->record_edit_member_id )->link()"}
{{else}}
{lang="cms_row_added_by" htmlsprintf="$row->author()->link()"}
{{endif}}
{{else}}
{{if $row->record_edit_time > 0}}
{lang="cms_row_updated_by" htmlsprintf="$row->lastCommenter()->link()"}
@@ -1945,7 +1991,11 @@
<li class="ipsType_light">
<a href='{$row->url( 'getLastComment' )}' title='{lang="get_last_post"}'>
{{if $row->mapped('num_comments')}}{datetime="$row->mapped('last_comment')"}{{else}}{datetime="$row->mapped('date')"}{{endif}}
{{if $row->database()->_comment_bump & \IPS\cms\Databases::BUMP_ON_COMMENT}}
{{if $row->mapped('num_comments')}}{datetime="$row->mapped('last_comment')"}{{else}}{datetime="$row->mapped('date')"}{{endif}}
{{else}}
{{if $row->record_edit_time > 0}}{datetime="$row->record_edit_time"}{{else}}{datetime="$row->mapped('date')"}{{endif}}
{{endif}}
</a>
</li>
{{else}}
+2 -2
View File
@@ -233,9 +233,9 @@
"comment": "",
"decimals": null,
"default": "0",
"length": 9,
"length": 10,
"name": "record_views",
"type": "MEDIUMINT",
"type": "INT",
"unsigned": false,
"values": [
+2 -2
View File
@@ -17,7 +17,7 @@
</td>
</tr>]]></template_content_html><template_data>$comment, $email</template_data><template_content_plaintext><![CDATA[{{$item = $comment->item();}}
{$email->language->addToStack("x_commented_cms", FALSE, array( 'sprintf' => array( $comment->author()->name, $item::database()->recordWord( 1 ) ) ) )}: {$comment->item()->_title} ({$comment->url()})
]]></template_content_plaintext></template><template><template_app>cms</template_app><template_name>digests__item</template_name><template_content_html><![CDATA[<tr style='border-bottom: 1px solid #eeeeee'>
]]></template_content_plaintext><template_pinned>0</template_pinned></template><template><template_app>cms</template_app><template_name>digests__item</template_name><template_content_html><![CDATA[<tr style='border-bottom: 1px solid #eeeeee'>
<td dir='{dir}' width='40' align='center' valign='top' class=' '>
<img src='{$entry->author()->get_photo( true, true )}' width='32' height='32' style='border: 1px solid #000; vertical-align: middle;'>
</td>
@@ -34,4 +34,4 @@
</div>
</td>
</tr>]]></template_content_html><template_data>$entry, $email</template_data><template_content_plaintext><![CDATA[{$email->language->addToStack("x_created_cms_in", FALSE, array( 'sprintf' => array( $entry->author()->name, $entry::database()->recordWord( 1 ), $entry->container()->_title ) ) )}: {$entry->_title} ({$entry->url()})
]]></template_content_plaintext></template></emails>
]]></template_content_plaintext><template_pinned>0</template_pinned></template></emails>
+12
View File
@@ -38,5 +38,17 @@
"topics": {
"type": "S",
"class": "\\IPS\\Theme\\class_forums_front_topics"
},
"Parser": {
"type": "C",
"class": "\\IPS\\Text\\Parser"
},
"forums": {
"type": "C",
"class": "\\IPS\\forums\\modules\\admin\\forums\\forums"
},
"Forum": {
"type": "C",
"class": "\\IPS\\forums\\Forum"
}
}
+73 -23
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<javascript app="cms">
<file javascript_app="cms" javascript_location="admin" javascript_path="controllers/blocks" javascript_name="ips.blocks.form.js" javascript_type="controller" javascript_version="102002" javascript_position="1000050"><![CDATA[/**
<file javascript_app="cms" javascript_location="admin" javascript_path="controllers/blocks" javascript_name="ips.blocks.form.js" javascript_type="controller" javascript_version="103005" javascript_position="1000100"><![CDATA[/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
@@ -142,7 +142,7 @@
}
});
}(jQuery, _));]]></file>
<file javascript_app="cms" javascript_location="admin" javascript_path="controllers/databases" javascript_name="ips.databases.download.js" javascript_type="controller" javascript_version="102002" javascript_position="1000100">/**
<file javascript_app="cms" javascript_location="admin" javascript_path="controllers/databases" javascript_name="ips.databases.download.js" javascript_type="controller" javascript_version="103005" javascript_position="1000150">/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
@@ -187,7 +187,7 @@
}
});
}(jQuery, _));</file>
<file javascript_app="cms" javascript_location="admin" javascript_path="controllers/databases" javascript_name="ips.databases.form.js" javascript_type="controller" javascript_version="102002" javascript_position="1000100">/**
<file javascript_app="cms" javascript_location="admin" javascript_path="controllers/databases" javascript_name="ips.databases.form.js" javascript_type="controller" javascript_version="103005" javascript_position="1000150">/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
@@ -250,7 +250,7 @@
}
});
}(jQuery, _));</file>
<file javascript_app="cms" javascript_location="front" javascript_path="controllers/external" javascript_name="ips.external.communication.js" javascript_type="controller" javascript_version="102002" javascript_position="1000050"><![CDATA[/**
<file javascript_app="cms" javascript_location="front" javascript_path="controllers/external" javascript_name="ips.external.communication.js" javascript_type="controller" javascript_version="103005" javascript_position="1000150"><![CDATA[/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
@@ -385,7 +385,7 @@
}
});
}(jQuery, _));]]></file>
<file javascript_app="cms" javascript_location="admin" javascript_path="controllers/fields" javascript_name="ips.fields.form.js" javascript_type="controller" javascript_version="102002" javascript_position="1000150">/**
<file javascript_app="cms" javascript_location="admin" javascript_path="controllers/fields" javascript_name="ips.fields.form.js" javascript_type="controller" javascript_version="103005" javascript_position="1000200">/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
@@ -422,7 +422,7 @@
}
});
}(jQuery, _));</file>
<file javascript_app="cms" javascript_location="admin" javascript_path="controllers/media" javascript_name="ips.media.main.js" javascript_type="controller" javascript_version="102002" javascript_position="1000200"><![CDATA[/**
<file javascript_app="cms" javascript_location="admin" javascript_path="controllers/media" javascript_name="ips.media.main.js" javascript_type="controller" javascript_version="103005" javascript_position="1000300"><![CDATA[/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
@@ -451,6 +451,8 @@
this.on( 'click', '[data-action="deleteSelected"]', this.deleteSelected );
this.on( 'keydown', '[data-role="mediaSearch"]', this.searchKeyPress );
this.on( 'submitDialog', '[data-role="uploadButton"]', this.dialogSubmitted );
this.on( 'submitDialog', '[data-role="replaceFile"]', this.replaceDialogSubmitted );
this.on( 'click', '[data-role="replaceFile"]', this.uploadNewFile );
this.on( window, 'resize', this.resizePanels );
this.setup();
},
@@ -515,6 +517,32 @@
ips.ui.flashMsg.show( ips.pluralize( ips.getString('mediaUploadedCount'), data.response.count ) );
},
/**
* Dialog submit handler from uploading process
* The dialog response will include an array of media items in this folder. We'll use
* this data to rebuild the file listing, thereby showing the new files immediately.
*
* @param {event} e Event object
* @param {object} data Event data object
* @returns {void}
*/
replaceDialogSubmitted: function (e, data) {
var newHTML = [];
var folderID = data.response.folderID;
_.each( data.response.rows, function (file, key) {
newHTML.push( file );
});
this._cachedFolders[ folderID ] = newHTML;
this._buildFileListing( folderID, newHTML );
delete this._cachedFileInfo[ data.response.fileID ];
ips.ui.flashMsg.show( ips.getString('mediaUploadedReplace') );
this._fileListing.find('[data-fileid="' + data.response.fileID + '"]').click();
},
/**
* keypress event handler for the search text box
*
@@ -691,6 +719,26 @@
this._checkDeleteButton();
},
/**
* Show the dialog to replace the current file we are viewing
*
* @returns {void}
*/
uploadNewFile: function( e ) {
if( e ){
e.preventDefault();
}
var itemId = this._getSelected().closest('[data-role="mediaItem"]').attr('data-fileid');
$( e.currentTarget ).ipsDialog( {
remoteSubmit: true,
forceReload: true,
url: $( e.currentTarget ).attr('data-baseUrl') + itemId,
title: ips.getString('replaceMediaFile')
});
},
/**
* Checks whether any files are selected and displays the delete button if so
*
@@ -832,15 +880,16 @@
this._sidebar.find('[data-role="multipleItemsMessage"]').html( language );
// Show the message box
this._sidebar.find('[data-role="itemInformation"]').hide().end().find('[data-role="multipleItems"]').show();
this._sidebar.find('[data-role="replaceFile"]').hide();
} else {
// Build the file info
var info = {
itemFilename: selected.attr('data-filename'),
itemUploaded: selected.attr('data-uploaded'),
itemTag: '{media="' + selected.attr('data-path') + '"}',
itemTag: '{media="' + selected.attr('data-fileid') + '"}',
itemID: selected.attr('data-fileid'),
itemIsImage: selected.is('[data-isImage]'),
itemIsImage: ( selected.attr('[data-fileType]') == 'image' ),
itemFilesize: null,
itemDimensions: null
};
@@ -899,6 +948,7 @@
// Hide the 'multiple items selected' div and show our info div
this._sidebar.find('[data-role="itemInformation"]').show().end().find('[data-role="multipleItems"]').hide();
this._sidebar.find('[data-role="replaceFile"]').show();
$( document ).trigger( 'contentChange', [ this._sidebar ] );
}
@@ -1010,7 +1060,7 @@
}
});
}(jQuery, _));]]></file>
<file javascript_app="cms" javascript_location="admin" javascript_path="controllers/pages" javascript_name="ips.pages.embed.js" javascript_type="controller" javascript_version="102002" javascript_position="1000250"><![CDATA[/**
<file javascript_app="cms" javascript_location="admin" javascript_path="controllers/pages" javascript_name="ips.pages.embed.js" javascript_type="controller" javascript_version="103005" javascript_position="1000250"><![CDATA[/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
@@ -1056,7 +1106,7 @@
}
});
}(jQuery, _));]]></file>
<file javascript_app="cms" javascript_location="admin" javascript_path="controllers/pages" javascript_name="ips.pages.form.js" javascript_type="controller" javascript_version="102002" javascript_position="1000250"><![CDATA[/**
<file javascript_app="cms" javascript_location="admin" javascript_path="controllers/pages" javascript_name="ips.pages.form.js" javascript_type="controller" javascript_version="103005" javascript_position="1000250"><![CDATA[/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
@@ -1149,7 +1199,7 @@
}
});
}(jQuery, _));]]></file>
<file javascript_app="cms" javascript_location="front" javascript_path="controllers/records" javascript_name="ips.records.form.js" javascript_type="controller" javascript_version="102002" javascript_position="1000100"><![CDATA[/**
<file javascript_app="cms" javascript_location="front" javascript_path="controllers/records" javascript_name="ips.records.form.js" javascript_type="controller" javascript_version="103005" javascript_position="1000100"><![CDATA[/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
@@ -1261,7 +1311,7 @@
}
});
}(jQuery, _));]]></file>
<file javascript_app="cms" javascript_location="front" javascript_path="controllers/records" javascript_name="ips.records.revisions.js" javascript_type="controller" javascript_version="102002" javascript_position="1000100">/**
<file javascript_app="cms" javascript_location="front" javascript_path="controllers/records" javascript_name="ips.records.revisions.js" javascript_type="controller" javascript_version="103005" javascript_position="1000100">/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
@@ -1293,7 +1343,7 @@
_.each( self.scope.find(&quot;[data-key]&quot;), function(elem){
if( $(elem).attr('data-method') == 'merge' )
{
CodeMirror.MergeView( document.getElementById( $(elem).attr('id') ), {
CodeMirror.MergeView( document.getElementById( $(elem).identify().attr('id') ), {
value: self.scope.find( '[data-original=&quot;' + $(elem).attr('data-key') + '&quot;]' ).val(),
origLeft: self.scope.find( '[data-current=&quot;' + $(elem).attr('data-key') + '&quot;]' ).val(),
lineWrapping: true,
@@ -1316,7 +1366,7 @@
}
});
}(jQuery, _));</file>
<file javascript_app="cms" javascript_location="admin" javascript_path="controllers/templates" javascript_name="ips.templates.addForm.js" javascript_type="controller" javascript_version="102002" javascript_position="1000300">/**
<file javascript_app="cms" javascript_location="admin" javascript_path="controllers/templates" javascript_name="ips.templates.addForm.js" javascript_type="controller" javascript_version="103005" javascript_position="1000050">/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
@@ -1366,7 +1416,7 @@
}
});
}(jQuery, _));</file>
<file javascript_app="cms" javascript_location="admin" javascript_path="controllers/templates" javascript_name="ips.templates.conflict.js" javascript_type="controller" javascript_version="102002" javascript_position="1000300">/**
<file javascript_app="cms" javascript_location="admin" javascript_path="controllers/templates" javascript_name="ips.templates.conflict.js" javascript_type="controller" javascript_version="103005" javascript_position="1000050">/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
@@ -1438,7 +1488,7 @@
}
});
}(jQuery, _));</file>
<file javascript_app="cms" javascript_location="admin" javascript_path="controllers/templates" javascript_name="ips.templates.fileEditor.js" javascript_type="controller" javascript_version="102002" javascript_position="1000300"><![CDATA[/**
<file javascript_app="cms" javascript_location="admin" javascript_path="controllers/templates" javascript_name="ips.templates.fileEditor.js" javascript_type="controller" javascript_version="103005" javascript_position="1000050"><![CDATA[/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
@@ -2304,7 +2354,7 @@
}
});
}(jQuery, _));]]></file>
<file javascript_app="cms" javascript_location="admin" javascript_path="controllers/templates" javascript_name="ips.templates.fileList.js" javascript_type="controller" javascript_version="102002" javascript_position="1000300"><![CDATA[/**
<file javascript_app="cms" javascript_location="admin" javascript_path="controllers/templates" javascript_name="ips.templates.fileList.js" javascript_type="controller" javascript_version="103005" javascript_position="1000050"><![CDATA[/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
@@ -2646,7 +2696,7 @@
}
});
}(jQuery, _));]]></file>
<file javascript_app="cms" javascript_location="admin" javascript_path="controllers/templates" javascript_name="ips.templates.main.js" javascript_type="controller" javascript_version="102002" javascript_position="1000300">/**
<file javascript_app="cms" javascript_location="admin" javascript_path="controllers/templates" javascript_name="ips.templates.main.js" javascript_type="controller" javascript_version="103005" javascript_position="1000050">/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
@@ -2687,7 +2737,7 @@
});
}(jQuery, _));</file>
<file javascript_app="cms" javascript_location="admin" javascript_path="controllers/templates" javascript_name="ips.templates.variablesDialog.js" javascript_type="controller" javascript_version="102002" javascript_position="1000300">/**
<file javascript_app="cms" javascript_location="admin" javascript_path="controllers/templates" javascript_name="ips.templates.variablesDialog.js" javascript_type="controller" javascript_version="103005" javascript_position="1000050">/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
@@ -2725,7 +2775,7 @@
}
});
}(jQuery, _));</file>
<file javascript_app="cms" javascript_location="front" javascript_path="mixins" javascript_name="ips.cms.builder.area.js" javascript_type="mixins" javascript_version="102002" javascript_position="1000150"><![CDATA[/**
<file javascript_app="cms" javascript_location="front" javascript_path="mixins" javascript_name="ips.cms.builder.area.js" javascript_type="mixins" javascript_version="103005" javascript_position="1000050"><![CDATA[/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
@@ -2794,7 +2844,7 @@
});
}(jQuery, _));]]></file>
<file javascript_app="cms" javascript_location="front" javascript_path="mixins" javascript_name="ips.cms.builder.block.js" javascript_type="mixins" javascript_version="102002" javascript_position="1000150"><![CDATA[/**
<file javascript_app="cms" javascript_location="front" javascript_path="mixins" javascript_name="ips.cms.builder.block.js" javascript_type="mixins" javascript_version="103005" javascript_position="1000050"><![CDATA[/**
* Invision Community
* (c) Invision Power Services, Inc. - https://www.invisioncommunity.com
*
@@ -2902,7 +2952,7 @@
});
}(jQuery, _));]]></file>
<file javascript_app="cms" javascript_location="admin" javascript_path="templates" javascript_name="ips.templates.media.js" javascript_type="template" javascript_version="102002" javascript_position="1000300"><![CDATA[ips.templates.set( 'templates.media.grid', "\
<file javascript_app="cms" javascript_location="admin" javascript_path="templates" javascript_name="ips.templates.media.js" javascript_type="template" javascript_version="103005" javascript_position="1000050"><![CDATA[ips.templates.set( 'templates.media.grid', "\
<ul class='ipsGrid' data-ipsGrid data-ipsGrid-minItemSize='100' data-ipsGrid-maxItemSize='200'>\
{{{contents}}}\
</ul>\
@@ -2919,7 +2969,7 @@ ips.templates.set( 'templates.media.noSearchResults', "\
{{#lang}}mediaNoResults{{/lang}}\
</div>\
");]]></file>
<file javascript_app="cms" javascript_location="admin" javascript_path="templates" javascript_name="ips.templates.templates.js" javascript_type="template" javascript_version="102002" javascript_position="1000300"><![CDATA[/* TEMPLATE EDITOR TEMPLATES */
<file javascript_app="cms" javascript_location="admin" javascript_path="templates" javascript_name="ips.templates.templates.js" javascript_type="template" javascript_version="103005" javascript_position="1000050"><![CDATA[/* TEMPLATE EDITOR TEMPLATES */
/* TEMPLATECEPTION */
ips.templates.set('templates.editor.newTab', " \
<li data-fileid='{{fileid}}' data-location='{{location}}'>\
+22 -5
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<language>
<app key="cms" version="102002">
<app key="cms" version="103005">
<word key="__app_cms" js="0">Pages</word>
<word key="menutab__cms" js="0">Pages</word>
<word key="menutab__cms_icon" js="0">files-o</word>
@@ -14,8 +14,13 @@
<word key="rebuilding_cms_database_reciprocal_map" js="0">Rebuilding %s reciprocal maps (field ID %s)</word>
<word key="rebuilding_cms_database_records" js="0">Rebuilding %s records</word>
<word key="rebuilding_cms_database_sync_topics" js="0">Rebuilding %s synchronized topics</word>
<word key="cms_page" js="0">Page</word>
<word key="cms_pages_pageitem_pl" js="0">Pages</word>
<word key="cms_page_pl_lc" js="0">pages</word>
<word key="cms_page_pl" js="0">Pages</word>
<word key="module_no_permission" js="0">No permission</word>
<word key="converting_pages_34x" js="0">Converting bbcode pages to HTML</word>
<word key="content_theme_dev_cannot_write_template" js="0">Could not write the file %s</word>
<word key="sitemap_cms_Databases" js="0">Database Records</word>
<word key="sitemap_cms_Pages" js="0">Pages</word>
<word key="sitemap_databases_count" js="0">Number of records to include</word>
@@ -33,7 +38,7 @@
<word key="can_unhide_record" js="0">Can unhide %s?</word>
<word key="can_view_hidden_record" js="0">Can view a hidden %s?</word>
<word key="can_move_record" js="0">Can move %s?</word>
<word key="can_move_record_desc" js="0">Will be able to move only between the categories they have permission to moderate, not to any other category.</word>
<word key="can_move_record_desc" js="0">Will be able to move only between the categories they can create new content within.</word>
<word key="can_lock_record" js="0">Can lock %s?</word>
<word key="can_unlock_record" js="0">Can unlock %s?</word>
<word key="can_reply_to_locked_record" js="0">Can reply to a locked %s?</word>
@@ -600,14 +605,14 @@
<word key="database_lang_ia" js="0">Indefinite Article</word>
<word key="database_lang_ia_desc" js="0">Default is 'a record'</word>
<word key="database_all_editable" js="0">Wiki-style editing</word>
<word key="database_all_editable_desc" js="0"><![CDATA[Enabling this option will allow <strong>any user to edit</strong> any non-locked item, similar to a wiki. When not enabled, users can only edit their own non-locked item (with appropriate permissions).]]></word>
<word key="database_all_editable_desc" js="0"><![CDATA[Enabling this option will allow <strong>any user to edit</strong> any non-locked item, regardless of category or database edit permissions, similar to a wiki. When not enabled, users can only edit their own non-locked item (with appropriate permissions).]]></word>
<word key="cms_bitoptions_indefinite_own_edit" js="0">Edit own records indefinitely</word>
<word key="cms_bitoptions_indefinite_own_edit_desc" js="0"><![CDATA[Enabling this option will allow the record author to edit their own unlocked and not hidden records indefinitely.<br />When not enabled, the <a href='{internal.app=core&module=members&controller=groups}'>member group</a> content editing permissions are used.]]></word>
<word key="database_revisions" js="0">Store revisions</word>
<word key="database_revisions_desc" js="0">When users or admins edit the items in this database, a copy of the original item will be stored which can be restored or compared later</word>
<word key="database_search" js="0">Allow searching</word>
<word key="database_comment_bump" js="0">Update the last action</word>
<word key="database_comment_bump_desc" js="0">The Last Action shows in the category view and is how items are sorted by default/can be used to sort items.</word>
<word key="database_comment_bump_desc" js="0">The last action shows in the category view and is how items are sorted by default/can be used to sort items. Note that adjusting this setting is not retroactive and will only apply to comments made or records that are edited moving forward.</word>
<word key="database_comment_bump_edit_comment" js="0">When the item is edited or a new comment is made</word>
<word key="database_comment_bump_edit" js="0">When the item is edited only</word>
<word key="database_comment_bump_comment" js="0">When a new comment is made only</word>
@@ -643,6 +648,7 @@
<word key="database_forum_record__rebuild" js="0"><![CDATA[<a href='%s' data-ipsDialog data-ipsDialog-title='Rebuild Synchronized Topic Content'>Rebuild synchronized topic content</a>.]]></word>
<word key="database_forum_comments" js="0">Use forum for comments</word>
<word key="database_forum_comments_desc" js="0">Comments will be posted as replies. If you change the way comments are stored, your existing comments will no longer be displayed. Comments are not copied to the forums or to the internal comments database if you change this setting.</word>
<word key="database_forum_comments__rebuild" js="0"><![CDATA[<a href='%s' data-ipsDialog data-ipsDialog-title='Rebuild Comment Counts'>Rebuild Comment Counts</a>.]]></word>
<word key="database_forum_forum" js="0">Forum</word>
<word key="database_forum_prefix" js="0">Topic prefix</word>
<word key="database_forum_suffix" js="0">Topic suffix</word>
@@ -693,7 +699,7 @@
<word key="cms_database_permissions" js="0">Database Permissions</word>
<word key="database_use_categories" js="0">Use categories</word>
<word key="database_use_categories_desc" js="0">You can optionally not use categories which means that the default home page for this database is the record list, and the category name is not added to the URL.</word>
<word key="database_use_categories_impossible" js="0">You have %s categories in this database. You can only have 1 category to store records in the database directly.</word>
<word key="database_use_categories_impossible" js="0">You have %s categories in this database. You can only have 1 category to store records in the database directly. If you wish to disable category support please move articles to a single category and delete the no longer needed categories.</word>
<word key="database_use_categories_yes" js="0"><![CDATA[<strong>Yes</strong>, allow records to be stored in different categories]]></word>
<word key="database_use_categories_no" js="0"><![CDATA[<strong>No</strong>, store records in the database directly]]></word>
<word key="cms_db_cats_disabled" js="0"><![CDATA[<em>Categories not used</em>]]></word>
@@ -729,6 +735,8 @@
<word key="database_rebuild_topics_desc" js="0">This tool will add a background task to resychronize the content generated in the first post of a topic created by this database. It will also create topics for records if a topic does not exist.</word>
<word key="database_rebuild_topics_button" js="0">Continue</word>
<word key="database_rebuild_added" js="0">Rebuild task added to the queue.</word>
<word key="database_rebuild_commentcount_desc" js="0">This tool will add a background task to recount the number of comments and reviews for each item.</word>
<word key="database_rebuild_commentcount_button" js="0">Continue</word>
<word key="menu__cms_pages_media" js="0">Media</word>
<word key="r__media" js="0">Media</word>
<word key="r__media_manage" js="0">View Media</word>
@@ -741,6 +749,7 @@
<word key="cms_search_media" js="0">Search media...</word>
<word key="media_folders" js="0">Folders</word>
<word key="media_file_overview" js="0">File Overview</word>
<word key="replace_media_file" js="0">Replace file...</word>
<word key="media_root_folder" js="0">Pages Media</word>
<word key="media_tag_title" js="0">Media Tag</word>
<word key="media_tag_desc" js="0">Insert this tag in your pages/templates to insert this media item.</word>
@@ -919,6 +928,7 @@
<word key="content_err_page_500" js="0">The page could not be displayed at this time. Please try again later.</word>
<word key="content_err_page_500_admin" js="0">There is a parse error in the page.</word>
<word key="cms_delete_linked_topic" js="0">This topic is used for %s comments and cannot be deleted</word>
<word key="cms_forum_used" js="0"><![CDATA[This forum is used by the Pages App to store comments for a database and cannot be deleted.<br/>Please remove the association from database %s to proceed]]></word>
<word key="digest_area_cms_cms" js="0">%s</word>
<word key="x_created_cms_in" js="0">%s posted a %s in %s</word>
<word key="x_created_cms_in_embed" js="0">%s posted %s in %s</word>
@@ -951,9 +961,14 @@
<word key="record_category_id" js="0">Category</word>
<word key="copy_comments" js="0">Copy posts?</word>
<word key="copy_comments_desc" js="0">Setting this to Yes will also copy all posts on the topic and add them as comments on the new %s</word>
<word key="copy_comments_assoc_desc" js="0">Setting this to Yes will associate the new %s with the existing topic, which will result in the existing posts being treated as comments on the %s</word>
<word key="copy_author" js="0">Use original author?</word>
<word key="copy_author_desc" js="0">Setting this to Yes will make the author of the new %s the same as the topic. Otherwise, your account will be used</word>
<word key="copying_comments" js="0">Copying Comments...</word>
<word key="comments_show_message" js="0">Show item message?</word>
<word key="comments_meta_message" js="0">Message</word>
<word key="comments_meta_color" js="0">Message color</word>
<word key="default_copyposts_message" js="0">Please be aware that these comments were copied here from another source and that the date and time shown for each comment may not be accurate.</word>
<word key="cms_download_db" js="1">Download Database</word>
<word key="cms_download_db_explain" js="1"><![CDATA[This will download a file with the database settings, templates, custom fields and categories which can be uploaded to another community.<br>Records are <em>not</em> included.]]></word>
<word key="index_as_categories" js="1">Show categories</word>
@@ -965,5 +980,7 @@
<word key="mediaErrorDeleting" js="1">There was an error deleting {!# [1:this file][?:these files]}</word>
<word key="mediaNoResults" js="1">No files match your search term</word>
<word key="mediaUploadedCount" js="1">{!# [1:A file was][?:# files were]} added to this folder</word>
<word key="replaceMediaFile" js="1">Replace File</word>
<word key="mediaUploadedReplace" js="1">The file has been updated</word>
</app>
</language>
+2 -2
View File
@@ -15,8 +15,8 @@
"protected": 0
},
"database": {
"default_controller": "",
"protected": false
"default_controller": "index",
"protected": 0
}
}
}
+100 -16
View File
@@ -391,7 +391,9 @@
]
}
},
"collation": "utf8mb4_unicode_ci"
"collation": "utf8mb4_unicode_ci",
"comment": "",
"reporting": "count"
},
"cms_containers": {
"name": "cms_containers",
@@ -1089,7 +1091,9 @@
}
},
"collation": "utf8mb4_unicode_ci",
"engine": "InnoDB"
"engine": "InnoDB",
"comment": "",
"reporting": "count"
},
"cms_database_categories": {
"name": "cms_database_categories",
@@ -1661,8 +1665,22 @@
"columns": [
"category_database_id"
]
},
"category_select": {
"type": "key",
"name": "category_select",
"columns": [
"category_parent_id",
"category_position"
],
"length": [
null,
null
]
}
}
},
"comment": "",
"reporting": "count"
},
"cms_database_comments": {
"name": "cms_database_comments",
@@ -1908,9 +1926,20 @@
"length": [
null
]
},
"comment_ip_address": {
"type": "key",
"name": "comment_ip_address",
"columns": [
"comment_ip_address"
],
"length": [
null
]
}
},
"comment": ""
"comment": "",
"reporting": "count"
},
"cms_database_fields": {
"name": "cms_database_fields",
@@ -2313,7 +2342,9 @@
}
},
"collation": "utf8mb4_unicode_ci",
"engine": "InnoDB"
"engine": "InnoDB",
"comment": "",
"reporting": "count"
},
"cms_database_revisions": {
"name": "cms_database_revisions",
@@ -2437,7 +2468,8 @@
]
}
},
"comment": ""
"comment": "",
"reporting": "count"
},
"cms_folders": {
"name": "cms_folders",
@@ -2545,7 +2577,9 @@
]
}
},
"collation": "utf8mb4_unicode_ci"
"collation": "utf8mb4_unicode_ci",
"comment": "",
"reporting": "count"
},
"cms_pages": {
"name": "cms_pages",
@@ -2868,7 +2902,7 @@
"type": "key",
"name": "page_full_path",
"length": [
250
191
],
"columns": [
"page_full_path"
@@ -2877,17 +2911,19 @@
"page_folder": {
"type": "key",
"name": "page_folder",
"length": [
null,
181
],
"columns": [
"page_folder_id",
"page_seo_name"
],
"length": [
null,
191
]
}
},
"collation": "utf8_unicode_ci"
"collation": "utf8mb4_unicode_ci",
"comment": "",
"reporting": "count"
},
"cms_templates": {
"name": "cms_templates",
@@ -3451,6 +3487,20 @@
"unsigned": false,
"values": [],
"zerofill": false
},
"review_author_response": {
"name": "review_author_response",
"type": "MEDIUMTEXT",
"length": null,
"decimals": null,
"values": null,
"allow_null": true,
"default": null,
"comment": "",
"unsigned": false,
"zerofill": false,
"auto_increment": false,
"binary": false
}
},
"indexes": {
@@ -3498,13 +3548,29 @@
"type": "key",
"name": "review_author",
"columns": [
"review_author",
"review_date"
],
"length": [
null,
null
]
},
"review_item": {
"type": "key",
"name": "review_item",
"columns": [
"review_item",
"review_author"
],
"length": [
null,
null
]
}
}
},
"comment": "",
"reporting": "count"
},
"cms_media": {
"name": "cms_media",
@@ -3634,7 +3700,9 @@
]
}
},
"collation": "utf8mb4_unicode_ci"
"collation": "utf8mb4_unicode_ci",
"comment": "",
"reporting": "count"
},
"cms_media_folders": {
"name": "cms_media_folders",
@@ -3718,7 +3786,9 @@
]
}
},
"collation": "utf8mb4_unicode_ci"
"collation": "utf8mb4_unicode_ci",
"comment": "",
"reporting": "count"
},
"cms_database_fields_reciprocal_map": {
"name": "cms_database_fields_reciprocal_map",
@@ -3881,6 +3951,20 @@
"unsigned": false,
"values": [],
"zerofill": false
},
"thumb_record_id": {
"allow_null": false,
"auto_increment": false,
"binary": false,
"comment": "",
"decimals": null,
"default": "0",
"length": 10,
"name": "thumb_record_id",
"type": "INT",
"unsigned": true,
"values": [],
"zerofill": false
}
},
"indexes": {
+24 -12
View File
@@ -1,50 +1,62 @@
[
{
"key": "cms_error_page",
"default": "0"
"default": "0",
"report": "bool"
},
{
"key": "cms_promote_cat",
"default": "0"
"default": "0",
"report": "bool"
},
{
"key": "cms_promote_enabled",
"default": "0"
"default": "0",
"report": "full"
},
{
"key": "cms_promote_groups",
"default": "*"
"default": "*",
"report": "bool"
},
{
"key": "cms_root_page_url",
"default": ""
"default": "",
"report": "bool"
},
{
"key": "cms_root_path",
"default": ""
"default": "",
"report": "bool"
},
{
"key": "cms_use_different_gateway",
"default": "0"
"default": "0",
"report": "full"
},
{
"key": "page_extension",
"default": ".html"
"default": ".html",
"report": "full"
},
{
"key": "sitemap_databases_count",
"default": "-1"
"default": "-1",
"report": "full"
},
{
"key": "sitemap_databases_priority",
"default": "1"
"default": "1",
"report": "full"
},
{
"key": "sitemap_pages_count",
"default": "-1"
"default": "-1",
"report": "full"
},
{
"key": "sitemap_pages_priority",
"default": "1"
"default": "1",
"report": "full"
}
]
+47 -31
View File
@@ -115,6 +115,15 @@
<a class="ipsPos_right ipsBadge ipsBadge_style1" href="{$page->url()}" target="_blank">{lang="cms_db_used_on_page" sprintf="$page->_title"}</a>
{{endif}}
<p class='ipsType_light'>{$database->_description}</p>]]></template>
<template template_group="databases" template_name="rebuildCommentCounts" template_data="$id" template_location="admin" template_app="cms"><![CDATA[
<div class='acpBlock'>
<div class="ipsPad">
{lang="database_rebuild_commentcount_desc"}
<p>
<a href='{url="app=cms&module=databases&controller=databases&do=rebuildCommentCounts&id={$id}&process=1"}' class="ipsButton ipsButton_primary">{lang="database_rebuild_commentcount_button"}</a>
</p>
</div>
</div>]]></template>
<template template_group="databases" template_name="rebuildTopics" template_data="$id" template_location="admin" template_app="cms"><![CDATA[
<div class='acpBlock'>
<div class="ipsPad">
@@ -153,24 +162,24 @@
<template template_group="global" template_name="embedRecord" template_data="$item, $url" template_location="front" template_app="cms"><![CDATA[
{{$image = NULL;}}
<div data-embedInfo-maxSize='500' class='ipsRichEmbed'>
{template="embedHeader" group="embed" app="core" params="$item, \IPS\Member::loggedIn()->language()->addToStack( 'x_created_cms_in_embed', FALSE, array( 'sprintf' => array( $item->author()->name, $item->indefiniteArticle(), $item->container()->_title ) ) ), $item->mapped('date')"}
{template="embedHeader" group="embed" app="core" params="$item, \IPS\Member::loggedIn()->language()->addToStack( 'x_created_cms_in_embed', FALSE, array( 'sprintf' => array( $item->author()->name, $item->indefiniteArticle(), $item->container()->_title ) ) ), $item->mapped('date'), $url"}
{{if $item->record_image}}
{{$image = \IPS\File::get( "cms_Records", $item->record_image );}}
{{elseif count( $item->contentImages() )}}
{{$attachType = key( $item->contentImages()[0] );}}
{{$image = \IPS\File::get( $attachType, $item->contentImages()[0][ $attachType ] );}}
{{elseif $contentImage = $item->contentImages(1)}}
{{$attachType = key( $contentImage[0] );}}
{{$image = \IPS\File::get( $attachType, $contentImage[0][ $attachType ] );}}
{{endif}}
{{if $image}}
<div class='ipsRichEmbed_masthead ipsRichEmbed_mastheadBg ipsType_center'>
<a href='{$item->url()}' style='background-image: url( "{expression="str_replace( array( '(', ')' ), array( '\(', '\)' ), $image->url )"}" )'>
<a href='{$url}' style='background-image: url( "{expression="str_replace( array( '(', ')' ), array( '\(', '\)' ), $image->url )"}" )'>
<img src='{$image->url}' class='ipsHide' alt=''>
</a>
</div>
{{endif}}
<div class='ipsPad'>
<div class='ipsPad_double'>
<h3 class='ipsRichEmbed_itemTitle ipsTruncate ipsTruncate_line ipsType_blendLinks'>
<a href='{$item->url()}' title="{$item->mapped('title')}">{$item->mapped('title')}</a>
<a href='{$url}' title="{$item->mapped('title')}">{$item->mapped('title')}</a>
</h3>
<div class='ipsType_richText ipsType_medium ipsSpacer_top ipsSpacer_half' data-truncate='3'>
{$item->truncated(TRUE)|raw}
@@ -191,20 +200,20 @@
</div>]]></template>
<template template_group="global" template_name="embedRecordComment" template_data="$comment, $item, $url" template_location="front" template_app="cms"><![CDATA[
<div data-embedInfo-maxSize='500' class='ipsRichEmbed'>
{template="embedHeader" group="embed" app="core" params="$comment, \IPS\Member::loggedIn()->language()->addToStack( 'x_commented_cms_embed', FALSE, array( 'sprintf' => array( $comment->author()->name, $item->indefiniteArticle() ) ) ), $comment->mapped('date')"}
<div class='ipsPad'>
{template="embedHeader" group="embed" app="core" params="$comment, \IPS\Member::loggedIn()->language()->addToStack( 'x_commented_cms_embed', FALSE, array( 'sprintf' => array( $comment->author()->name, $item->indefiniteArticle() ) ) ), $comment->mapped('date'), $url"}
<div class='ipsPad_double'>
<div class='ipsRichEmbed_originalItem ipsAreaBackground_reset ipsSpacer_bottom ipsType_blendLinks'>
<div>
{{if $item->record_image}}
{{$image = \IPS\File::get( "cms_Records", $item->record_image );}}
{{elseif count( $item->contentImages() )}}
{{$attachType = key( $item->contentImages()[0] );}}
{{$image = \IPS\File::get( $attachType, $item->contentImages()[0][ $attachType ] );}}
{{elseif $contentImage = $item->contentImages(1)}}
{{$attachType = key( $contentImage[0] );}}
{{$image = \IPS\File::get( $attachType, $contentImage[0][ $attachType ] );}}
{{endif}}
{{if $image}}
<div class='ipsRichEmbed_masthead ipsRichEmbed_mastheadBg ipsType_center'>
<a href='{$item->url()}' style='background-image: url( "{expression="str_replace( array( '(', ')' ), array( '\(', '\)' ), $image->url )"}" )'>
<a href='{$url}' style='background-image: url( "{expression="str_replace( array( '(', ')' ), array( '\(', '\)' ), $image->url )"}" )'>
<img src='{$image->url}' class='ipsHide' alt=''>
</a>
</div>
@@ -219,7 +228,7 @@
{$comment->truncated(TRUE)|raw}
</div>
{{if \IPS\IPS::classUsesTrait( $comment, 'IPS\Content\Reactable' ) and count( $comment->reactions() ) and settings.reputation_enabled}}
{{if settings.reputation_enabled and \IPS\IPS::classUsesTrait( $comment, 'IPS\Content\Reactable' ) and count( $comment->reactions() )}}
<ul class='ipsList_inline ipsSpacer_top ipsSpacer_half'>
<li>
{template="reactionOverview" app="core" group="global" params="$comment, TRUE, 'small'"}
@@ -240,20 +249,20 @@
{{endif}}]]></template>
<template template_group="global" template_name="embedRecordReview" template_data="$comment, $item, $url" template_location="front" template_app="cms"><![CDATA[
<div data-embedInfo-maxSize='500' class='ipsRichEmbed'>
{template="embedHeader" group="embed" app="core" params="$comment, \IPS\Member::loggedIn()->language()->addToStack( 'x_reviewed_cms_embed', FALSE, array( 'sprintf' => array( $comment->author()->name, $item->indefiniteArticle() ) ) ), $comment->mapped('date')"}
<div class='ipsPad'>
{template="embedHeader" group="embed" app="core" params="$comment, \IPS\Member::loggedIn()->language()->addToStack( 'x_reviewed_cms_embed', FALSE, array( 'sprintf' => array( $comment->author()->name, $item->indefiniteArticle() ) ) ), $comment->mapped('date'), $url"}
<div class='ipsPad_double'>
<div class='ipsRichEmbed_originalItem ipsAreaBackground_reset ipsSpacer_bottom ipsType_blendLinks'>
<div>
{{if $item->record_image}}
{{$image = \IPS\File::get( "cms_Records", $item->record_image );}}
{{elseif count( $item->contentImages() )}}
{{$attachType = key( $item->contentImages()[0] );}}
{{$image = \IPS\File::get( $attachType, $item->contentImages()[0][ $attachType ] );}}
{{elseif $contentImage = $item->contentImages(1)}}
{{$attachType = key( $contentImage[0] );}}
{{$image = \IPS\File::get( $attachType, $contentImage[0][ $attachType ] );}}
{{endif}}
{{if $image}}
<div class='ipsRichEmbed_masthead ipsRichEmbed_mastheadBg ipsType_center'>
<a href='{$item->url()}' style='background-image: url( "{expression="str_replace( array( '(', ')' ), array( '\(', '\)' ), $image->url )"}" )'>
<a href='{$url}' style='background-image: url( "{expression="str_replace( array( '(', ')' ), array( '\(', '\)' ), $image->url )"}" )'>
<img src='{$image->url}' class='ipsHide' alt=''>
</a>
</div>
@@ -273,7 +282,7 @@
{$comment->truncated(TRUE)|raw}
</div>
{{if \IPS\IPS::classUsesTrait( $comment, 'IPS\Content\Reactable' ) and count( $comment->reactions() ) and settings.reputation_enabled}}
{{if settings.reputation_enabled and \IPS\IPS::classUsesTrait( $comment, 'IPS\Content\Reactable' ) and count( $comment->reactions() )}}
<ul class='ipsList_inline ipsSpacer_top ipsSpacer_half'>
<li>
{template="reactionOverview" app="core" group="global" params="$comment, TRUE, 'small'"}
@@ -293,11 +302,11 @@
{{endif}}]]></template>
<template template_group="media" template_name="fileListing" template_data="$url, $item" template_location="admin" template_app="cms"><![CDATA[
{{if $item instanceof \IPS\cms\Media}}
<li class='ipsGrid_span4' data-role='mediaItem' data-fileid='{$item->id}' data-uploaded='{datetime="$item->added" dateonly="true"}' data-path='{$item->full_path}' data-filename='{$item->filename}' data-url='{$item->url()}' {{if $item->is_image}}data-isImage{{endif}}>
<li class='ipsGrid_span4' data-role='mediaItem' data-fileid='{$item->id}' data-uploaded='{datetime="$item->added" dateonly="true"}' data-path='{$item->full_path}' data-filename='{$item->filename}' data-url='{$item->url()}?_cb={expression="time()"}' data-fileType='{{if $item->is_image}}image{{else}}file{{endif}}'>
<div class='cMedia_item {{if $item->is_image}}cMedia_itemImage{{endif}}'>
<div class='ipsAreaBackground_light'>
{{if $item->is_image}}
<img src='{$item->url()}'>
<img src='{$item->url()}?_cb={expression="time()"}'>
{{else}}
<div class='ipsNoThumb'></div>
{{endif}}
@@ -358,6 +367,7 @@
<input type='text' class='ipsField_fullWidth' value='' data-role='itemTag'>
<p class='ipsType_small ipsType_light'>{lang="media_tag_desc"}</p>
</div>
<hr class='ipsHr'>
<ul class='ipsList_reset ipsSpacer_top'>
@@ -378,6 +388,12 @@
<p class='ipsType_reset ipsType_medium' data-role='itemDimensions'></p>
</li>
</ul>
<hr class='ipsHr'>
<div class='ipsSpacer_top'>
<a href='#' class='ipsType_medium' data-role='replaceFile' data-baseUrl="{url="app=cms&module=pages&controller=media&do=replace&id="}">{lang="replace_media_file"}</a>
</div>
</div>
<div data-role='multipleItems'>
<div class='ipsPad ipsType_large ipsType_light ipsType_center ipsSpacer_top ipsSpacer_double' data-role='multipleItemsMessage'></div>
@@ -389,7 +405,7 @@
<div class='cMedia_managerToolbar'>
<ul class='ipsToolList ipsToolList_horizontal'>
{{if $rootButtons['add_page']}}
<li class='ipsPos_left'><a href='{url="app=cms&module=pages&controller=media&do=upload"}' data-ipsDialog data-ipsDialog-forceReload data-ipsDialog-remoteSubmit data-ipsDialog-title='{lang="cms_add_media"}' class='ipsButton ipsButton_normal ipsButton_small ipsButton_fullWidth' data-role='uploadButton'><i class='fa fa-cloud-upload'></i> &nbsp;{lang="cms_add_media"}</a></li>
<li class='ipsPos_left'><a href='{url="app=cms&module=pages&controller=media&do=upload"}' data-ipsDialog data-ipsDialog-forceReload data-ipsDialog-remoteSubmit data-ipsDialog-title='{lang="cms_add_media"}' class='ipsButton ipsButton_primary ipsButton_small ipsButton_fullWidth' data-role='uploadButton'><i class='fa fa-cloud-upload'></i> &nbsp;{lang="cms_add_media"}</a></li>
{{endif}}
<li class='ipsPos_left ipsHide' data-action='deleteFolder'><a href='{url="app=cms&module=pages&controller=media&do=delete"}' data-ipsDialog data-ipsDialog-forceReload data-ipsDialog-remoteVerify="false" data-ipsDialog-title='{lang="delete"}' class='ipsButton ipsButton_negative ipsButton_small ipsButton_fullWidth'><i class='fa fa-times'></i> {lang="cms_delete_folder_media"}</a></li>
<li class='ipsPos_left ipsHide' data-action='deleteSelected'><a href='#' class='ipsButton ipsButton_negative ipsButton_small ipsButton_fullWidth'><i class='fa fa-times'></i> {lang="cms_delete_selected_media"}</a></li>
@@ -453,7 +469,7 @@
{{endforeach}}
{{if $uploadField}}
<input type="hidden" name="MAX_FILE_SIZE" value="{$uploadField}">
<input type="hidden" name="plupload" value="{expression="md5( uniqid() )"}">
<input type="hidden" name="plupload" value="{expression="md5( mt_rand() )"}">
{{endif}}
<div class='ipsComposeArea ipsComposeArea_withPhoto ipsClearfix ipsContained'>
<div class='ipsPos_left ipsResponsive_hidePhone ipsResponsive_block'>{template="userPhoto" app="core" group="global" params="\IPS\Member::loggedIn(), 'small'"}</div>
@@ -541,7 +557,7 @@
</span>
{{endif}}
{$title}]]></template>
<template template_group="records" template_name="youtube" template_data="$url, $params" template_location="global" template_app="cms"><![CDATA[<div class='ipsEmbeddedVideo {{if \IPS\Settings::i()->max_video_width > 0}}ipsEmbeddedVideo_limited{{endif}}' contenteditable="false"><div><iframe id="ytplayer" type="text/html" {{foreach $params as $k => $v }}{$k}="{$v}" {{endforeach}} src="$url"></iframe></div></div>]]></template>
<template template_group="records" template_name="youtube" template_data="$url, $params" template_location="global" template_app="cms"><![CDATA[<div class='ipsEmbeddedVideo' contenteditable="false"><div><iframe id="ytplayer" type="text/html" {{foreach $params as $k => $v }}{$k}="{$v}" {{endforeach}} src="$url"></iframe></div></div>]]></template>
<template template_group="revisions" template_name="rows" template_data="$table, $headers, $rows" template_location="front" template_app="cms"><![CDATA[{{foreach $rows as $row}}
<article class='ipsComment ipsComment_parent ipsClearfix ipsClear'>
<div class='ipsComment_header ipsPhotoPanel ipsPhotoPanel_mini ipsPhotoPanel_notPhone'>
@@ -572,7 +588,7 @@
<div class='ipsGrid_span6 ipsPad' data-current='{$key}'>{$diff['current']}</div>
</div>
{{else}}
<div class='ipsPagesDiff' data-key='{$key}' data-method='merge' id="{expression="md5(uniqid())"}"></div>
<div class='ipsPagesDiff' data-key='{$key}' data-method='merge'></div>
<textarea class='ipsHide' data-original='{$key}'>{$diff['original']}</textarea>
<textarea class='ipsHide' data-current='{$key}'>{$diff['current']}</textarea>
{{endif}}
@@ -659,7 +675,7 @@
<div class='ipsGrid_span6 ipsPad' data-current='{$key}'>{$data['current']}</div>
</div>
{{else}}
<div class='ipsPagesDiff' data-key='{$key}' data-method='merge' id="{expression="md5(uniqid())"}"></div>
<div class='ipsPagesDiff' data-key='{$key}' data-method='merge'></div>
<textarea class='ipsHide' data-original='{$key}'>{$data['original']}</textarea>
<textarea class='ipsHide' data-current='{$key}'>{$data['current']}</textarea>
{{endif}}
@@ -717,7 +733,7 @@
{{endforeach}}
{{if $uploadField}}
<input type="hidden" name="MAX_FILE_SIZE" value="{$uploadField}">
<input type="hidden" name="plupload" value="{expression="md5( uniqid() )"}">
<input type="hidden" name="plupload" value="{expression="md5( mt_rand() )"}">
{{endif}}
{{if $hasModOptions}}
@@ -1054,7 +1070,7 @@
{{foreach $items as $item}}
<li class='ipsDataItem'>
<div class='ipsDataItem_main'>
<div class='ipsType_break ipsContained'><a href="{$item['link']}" target="_blank" rel="noopener noreferrer" class='ipsTruncate ipsTruncate_line'>{$item['title']}</a></div>
<div class='ipsType_break ipsContained'><a href="{$item['link']}" target="_blank" rel="noopener" class='ipsTruncate ipsTruncate_line'>{$item['title']}</a></div>
<span class='ipsType_light ipsType_small'>{datetime="$item['date']"}</span>
</div>
</li>
@@ -1323,7 +1339,7 @@ html[dir="rtl"] .cMedia_manager > .ipsColumns > .ipsColumn_fluid .cMedia_manager
}]]></css>
<css css_location="front" css_app="cms" css_attributes="" css_path="pages" css_name="page.css">/* ----- */
.ipsWidget.ipsWidget_horizontal[data-blockid^=&quot;app_cms&quot;] {
.ipsWidget.ipsWidget_horizontal[data-blockid^=&quot;app_cms&quot;]:not([data-blockid^=&quot;app_cms_RecordFeed&quot;]) {
padding: 0px !important;
background: transparent;
border: none;
+52 -1
View File
@@ -206,5 +206,56 @@
"101117": "4.2.0 Beta 8",
"102000": "4.2.0",
"102001": "4.2.0",
"102002": "4.2.0"
"102002": "4.2.0",
"102003": "4.2.0",
"102004": "4.2.1",
"102005": "4.2.2 Beta 1",
"102006": "4.2.2 Beta 2",
"102007": "4.2.2",
"102008": "4.2.2",
"102009": "4.2.2",
"102010": "4.2.3 Beta 1",
"102011": "4.2.3",
"102012": "4.2.3",
"102013": "4.2.3",
"102014": "4.2.4 Beta 1",
"102015": "4.2.4 Beta 2",
"102016": "4.2.4",
"102017": "4.2.4",
"102018": "4.2.4",
"102019": "4.2.4",
"102020": "4.2.4",
"102021": "4.2.5 Beta 1",
"102022": "4.2.5 Beta 2",
"102023": "4.2.5 Beta 3",
"102024": "4.2.5",
"102025": "4.2.5",
"102026": "4.2.5",
"102027": "4.2.5",
"102028": "4.2.5",
"102029": "4.2.5",
"102030": "4.2.6 Beta 1",
"102031": "4.2.6 Beta 1",
"102032": "4.2.6 Beta 2",
"102033": "4.2.6 Beta 3",
"102034": "4.2.6",
"102035": "4.2.6",
"102036": "4.2.6",
"102037": "4.2.7 Beta 1",
"102038": "4.2.7 Beta 2",
"102039": "4.2.7 Beta 3",
"102040": "4.2.7 Beta 4",
"102041": "4.2.7 Beta 5",
"102042": "4.2.7",
"102043": "4.2.7",
"102044": "4.2.7",
"102045": "4.2.7",
"102046": "4.2.7",
"102047": "4.2.8",
"102048": "4.2.8",
"103000": "4.3.0 Beta 1",
"103002": "4.3.0 Beta 2",
"103003": "4.3.0 Beta 3",
"103004": "4.3.0 Beta 4",
"103005": "4.3.0"
}
@@ -0,0 +1,44 @@
<?php
/**
* @brief Content Router extension: Pages
* @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 Pages
* @since 15 Dec 2017
*/
namespace IPS\cms\extensions\core\ContentRouter;
/* 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;
}
/**
* @brief Content Router extension: Pages
*/
class _Pages
{
/**
* @brief Content Item Classes
*/
public $classes = array();
/**
* Constructor
*
* @param \IPS\Member|IPS\Member\Group|NULL $memberOrGroup If checking access, the member/group to check for, or NULL to not check access
* @return void
*/
public function __construct( $memberOrGroup = NULL )
{
if ( $memberOrGroup === NULL or $memberOrGroup->canAccessModule( \IPS\Application\Module::get( 'cms', 'pages', 'front' ) ) )
{
$this->classes[] = 'IPS\cms\Pages\PageItem';
}
}
}
@@ -34,15 +34,24 @@ class _Records
foreach( \IPS\cms\Databases::databases() as $database )
{
if ( $database->page_id > 0 and $database->can('view') )
$theOnlyCategory = NULL;
if ( $database->page_id > 0 and $database->can('add') )
{
$catClass = '\IPS\cms\Categories' . $database->id;
if ( $database->can('add') )
if ( $catClass::canOnAny('add') )
{
try
{
$page = \IPS\cms\Pages\Page::load( $database->page_id );
if( $database->use_categories AND $theOnlyCategory = $catClass::theOnlyNode() )
{
$items[ 'cms_create_menu_records_' . $database->id ] = array(
'link' => $theOnlyCategory->url()->setQueryString('do', 'form')
);
continue;
}
$items[ 'cms_create_menu_records_' . $database->id ] = array(
'link' => $page->url()->setQueryString('do', 'form'),
'extraData' => ( $database->use_categories ) ? array( 'data-ipsDialog' => true, 'data-ipsDialog-size' => "narrow" ) : array(),
@@ -26,7 +26,7 @@ class _Cms
/**
* Anything need building?
*
* @return array
* @return bool
*/
public function toBuild()
{
@@ -37,7 +37,7 @@ class _Cms
/**
* Designer's mode on
*
* @return array
* @return bool
*/
public function on( $data=NULL )
{
@@ -51,7 +51,7 @@ class _Cms
/**
* Designer's mode off
*
* @return array
* @return bool
*/
public function off( $data=NULL )
{
@@ -23,6 +23,11 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
*/
class _Records
{
/**
* @brief Flag to indicate we don't want to be listed as a selectable area when configuring buttons
*/
public static $buttonLocation = FALSE;
/**
* Can we use HTML in this editor?
*
@@ -70,7 +75,7 @@ class _Records
try
{
$recordClass = '\IPS\cms\Records' . str_replace( '-review', '', $id3 );
$recordClass = '\IPS\cms\Records' . $id3;
return $recordClass::load( $id1 )->canView( $member );
}
@@ -95,7 +100,7 @@ class _Records
{
if ( $id3 )
{
$recordClass = '\IPS\cms\Records' . str_replace( '-review', '', $id3 );
$recordClass = '\IPS\cms\Records' . $id3;
$return = $recordClass::load( $id1 );
$return->url(); // Need to check that won't throw an exception later, which might happen if the database no longer has a page
return $return;
@@ -74,8 +74,6 @@ class _Media
if ( $new = \IPS\File::repairUrl( $media['media_file_object'] ) )
{
\IPS\Db::i()->update( 'cms_media', array( 'media_file_object' => $new ), array( 'media_id=?', $media['media_id'] ) );
unset( \IPS\Data\Store::i()->emoticons );
}
}
Loaded 100 of 1349 files, more files were not shown because too many files have changed in this diff. Show more