1886 lines
49 KiB
PHP
1886 lines
49 KiB
PHP
<?php
|
|
/**
|
|
* @brief Node Controller
|
|
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
|
* @copyright (c) Invision Power Services, Inc.
|
|
* @license https://www.invisioncommunity.com/legal/standards/
|
|
* @package Invision Community
|
|
* @since 18 Feb 2013
|
|
*/
|
|
|
|
namespace IPS\Node;
|
|
|
|
/* To prevent PHP errors (extending class does not exist) revealing path */
|
|
|
|
use IPS\Content\Item;
|
|
use IPS\Data\Store;
|
|
use IPS\Db;
|
|
use IPS\Dispatcher\Controller as DispatcherController;
|
|
use IPS\Helpers\Form;
|
|
use IPS\Helpers\Form\Checkbox;
|
|
use IPS\Helpers\Form\Date;
|
|
use IPS\Helpers\Form\Matrix;
|
|
use IPS\Helpers\Form\Node as FormNode;
|
|
use IPS\Helpers\Form\Number;
|
|
use IPS\Helpers\Form\Radio;
|
|
use IPS\Helpers\Form\YesNo;
|
|
use IPS\Helpers\MultipleRedirect;
|
|
use IPS\Helpers\Tree\Tree;
|
|
use IPS\Helpers\Wizard;
|
|
use IPS\Http\Url;
|
|
use IPS\IPS;
|
|
use IPS\Log;
|
|
use IPS\Member;
|
|
use IPS\Member\Group;
|
|
use IPS\Output;
|
|
use IPS\Request;
|
|
use IPS\Session;
|
|
use IPS\Settings;
|
|
use IPS\Task;
|
|
use IPS\Theme;
|
|
use LogicException;
|
|
use OutOfRangeException;
|
|
use UnderflowException;
|
|
use function count;
|
|
use function defined;
|
|
use function get_class;
|
|
use function in_array;
|
|
use function is_array;
|
|
use function is_numeric;
|
|
use function is_object;
|
|
|
|
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
|
{
|
|
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Node Controller
|
|
*/
|
|
class Controller extends DispatcherController
|
|
{
|
|
/**
|
|
* Title can contain HTML?
|
|
*/
|
|
public bool $_titleHtml = FALSE;
|
|
|
|
/**
|
|
* Description can contain HTML?
|
|
*/
|
|
public bool $_descriptionHtml = FALSE;
|
|
|
|
/**
|
|
* @brief If true, will prevent any item from being moved out of its current parent, only allowing them to be reordered within their current parent
|
|
*/
|
|
protected bool $lockParents = FALSE;
|
|
|
|
/**
|
|
* @brief If true, root cannot be turned into sub-items, and other items cannot be turned into roots
|
|
*/
|
|
protected bool $protectRoots = FALSE;
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected string $title = '';
|
|
|
|
/**
|
|
* @var bool
|
|
*/
|
|
protected bool $sortable = true;
|
|
|
|
/**
|
|
* Execute
|
|
*
|
|
* @return void
|
|
*/
|
|
public function execute() : void
|
|
{
|
|
/* Are we sortable? */
|
|
$nodeClass = $this->nodeClass;
|
|
$this->sortable = ( $nodeClass::$databaseColumnOrder and $nodeClass::$nodeSortable );
|
|
|
|
/* Set the title */
|
|
$title = $nodeClass::$nodeTitle;
|
|
|
|
if( Request::i()->subnode == 1 AND isset( $nodeClass::$subnodeClass ) )
|
|
{
|
|
$subnodeClass = $nodeClass::$subnodeClass;
|
|
$title = $subnodeClass::$nodeTitle;
|
|
}
|
|
|
|
$this->title = $title;
|
|
Output::i()->title = Member::loggedIn()->language()->addToStack( $title );
|
|
|
|
/* Do stuff */
|
|
parent::execute();
|
|
}
|
|
|
|
/**
|
|
* Manage
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function manage() : void
|
|
{
|
|
$nodeClass = $this->nodeClass;
|
|
|
|
if ( isset( Request::i()->searchResult ) )
|
|
{
|
|
try
|
|
{
|
|
Output::i()->output .= Theme::i()->getTemplate( 'global', 'core' )->message( sprintf( Member::loggedIn()->language()->get('search_results_in_nodes'), mb_strtolower( Member::loggedIn()->language()->get( $nodeClass::$nodeTitle . '_sg' ) ) ), 'information' );
|
|
}
|
|
catch( UnderflowException $ex )
|
|
{
|
|
Log::log( $ex->getMessage(), "Language" );
|
|
}
|
|
}
|
|
|
|
if ( $nodeClass::$databaseColumnParent === NULL )
|
|
{
|
|
$this->protectRoots = TRUE;
|
|
}
|
|
|
|
$tree = new Tree( $this->url, $nodeClass::$nodeTitle, array( $this, '_getRoots' ), array( $this, '_getRow' ), array( $this, '_getRowParentId' ), array( $this, '_getChildren' ), array( $this, '_getRootButtons' ), TRUE, $this->lockParents, $this->protectRoots, $this->_getRootsPerPage(), array( $this, '_getTotalRoots' ) );
|
|
Output::i()->output .= $tree;
|
|
|
|
if( IPS::classUsesTrait( $nodeClass, Grouping::class ) )
|
|
{
|
|
Output::i()->sidebar['actions']['groups'] = [
|
|
'icon' => 'folder-open',
|
|
'title' => 'node_groups',
|
|
'link' => $this->url->setQueryString( 'do', 'nodeGroups' )
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return int|null
|
|
*/
|
|
public function _getRootsPerPage(): ?int
|
|
{
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get Root Rows
|
|
*
|
|
* @return array
|
|
*/
|
|
public function _getRoots(): array
|
|
{
|
|
$nodeClass = $this->nodeClass;
|
|
$rows = array();
|
|
foreach( $nodeClass::roots( NULL ) as $node )
|
|
{
|
|
$rows[ $node->_id ] = $this->_getRow($node);
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
/**
|
|
* @return int
|
|
*/
|
|
public function _getTotalRoots(): int
|
|
{
|
|
$nodeClass = $this->nodeClass;
|
|
return count( $nodeClass::roots( NULL ) );
|
|
}
|
|
|
|
/**
|
|
* Show the "add" button in the page root rather than the table root
|
|
*/
|
|
protected bool $_addButtonInRoot = TRUE;
|
|
|
|
/**
|
|
* Get Root Buttons
|
|
*
|
|
* @return array
|
|
*/
|
|
public function _getRootButtons(): array
|
|
{
|
|
$nodeClass = $this->nodeClass;
|
|
|
|
if ( $nodeClass::canAddRoot() )
|
|
{
|
|
$add = array(
|
|
'icon' => 'plus',
|
|
'title' => 'add',
|
|
'link' => $this->url->setQueryString( 'do', 'form' ),
|
|
'data' => ( $nodeClass::$modalForms ? array( 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack('add') ) : array() )
|
|
);
|
|
|
|
if ( Request::i()->isAjax() or !$this->_addButtonInRoot )
|
|
{
|
|
return array( 'add' => $add );
|
|
}
|
|
else
|
|
{
|
|
Output::i()->sidebar['actions']['add'] = ( array( 'primary' => true ) + $add );
|
|
}
|
|
}
|
|
return array();
|
|
}
|
|
|
|
/**
|
|
* Return the custom badge for each row
|
|
*
|
|
* @param Model $node Node returned from $nodeClass::load()
|
|
* @return NULL|array Null for no badge, or an array of badge data (0 => CSS class type, 1 => language string, 2 => optional raw HTML to show instead of language string)
|
|
*/
|
|
public function _getRowBadge( Model $node ): ?array
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* Fetch any additional HTML for this row
|
|
*
|
|
* @param object $node Node returned from $nodeClass::load()
|
|
* @return NULL|string
|
|
*/
|
|
public function _getRowHtml( object $node ): ?string
|
|
{
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get Single Row
|
|
*
|
|
* @param mixed $id May be ID number (or key) or an \IPS\Node\Model object
|
|
* @param bool $root Format this as the root node?
|
|
* @param bool $noSort If TRUE, sort options will be disabled (used for search results)
|
|
* @return string
|
|
*/
|
|
public function _getRow( mixed $id, bool $root=FALSE, bool $noSort=FALSE ): string
|
|
{
|
|
$nodeClass = $this->nodeClass;
|
|
if ( $id instanceof Model)
|
|
{
|
|
$node = $id;
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
$node = $nodeClass::load( $id );
|
|
}
|
|
catch( OutOfRangeException $e )
|
|
{
|
|
Output::i()->error( 'node_error', '2S101/P', 404, '' );
|
|
}
|
|
}
|
|
|
|
$id = ( $node instanceof $nodeClass ) ? $node->_id : "s.{$node->_id}";
|
|
|
|
/* @var Model $class */
|
|
$class = get_class( $node );
|
|
|
|
$buttons = $node->getButtons($this->url, !($node instanceof $this->nodeClass));
|
|
if ( isset( Request::i()->searchResult ) and isset( $buttons['edit'] ) )
|
|
{
|
|
$buttons['edit']['link'] = $buttons['edit']['link']->setQueryString( 'searchResult', Request::i()->searchResult );
|
|
}
|
|
if ( isset( $buttons['delete'] ) and isset( Request::i()->root ) and Request::i()->root == $id )
|
|
{
|
|
unset( $buttons['delete']['data']['delete'] );
|
|
$buttons['delete']['data']['confirm'] = '';
|
|
}
|
|
|
|
return Theme::i()->getTemplate( 'trees', 'core' )->row(
|
|
$this->url,
|
|
$id,
|
|
static::nodeTitle( $node ),
|
|
$node->childrenCount( NULL ),
|
|
$buttons,
|
|
$node->_description,
|
|
$node->_icon ? $node->_icon : NULL,
|
|
( $noSort === FALSE and $class::$nodeSortable and $node->canEdit() ) ? $node->_position : NULL,
|
|
$root,
|
|
$node->_enabled,
|
|
( $node->_locked or !$node->canEdit() ),
|
|
( ( $node instanceof Model) ? $node->_badge : $this->_getRowBadge( $node ) ),
|
|
$this->_titleHtml,
|
|
$this->_descriptionHtml,
|
|
$node->canAdd(),
|
|
$this->_canBeRoot( $node ),
|
|
$this->_getRowHtml( $node ) . $node->ui( 'rowHtml' ),
|
|
isset( $node->_lockedLang ) ? $node->_lockedLang : null,
|
|
isset( $class::$featureColumnName ),
|
|
isset( $class::$featureColumnName ) ? $node->_featureColor : null
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get Row parent ID
|
|
*
|
|
* @param mixed $id Row ID
|
|
* @return mixed Parent ID
|
|
*/
|
|
public function _getRowParentId( mixed $id ): mixed
|
|
{
|
|
$nodeClass = $this->nodeClass;
|
|
|
|
try
|
|
{
|
|
return $nodeClass::load( $id )->parent();
|
|
}
|
|
catch( OutOfRangeException $e )
|
|
{
|
|
Output::i()->error( 'node_error', '2S101/Q', 404, '' );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get Child Rows
|
|
*
|
|
* @param int|string $id Row ID
|
|
* @return array
|
|
*/
|
|
public function _getChildren( int|string $id ): array
|
|
{
|
|
$rows = array();
|
|
|
|
$nodeClass = $this->nodeClass;
|
|
|
|
try
|
|
{
|
|
$node = $nodeClass::load( $id );
|
|
}
|
|
catch( OutOfRangeException $e )
|
|
{
|
|
Output::i()->error( 'node_error', '2S101/R', 404, '' );
|
|
}
|
|
|
|
foreach ( $node->children( NULL ) as $child )
|
|
{
|
|
$id = ( $child instanceof $this->nodeClass ? '' : 's.' ) . $child->_id;
|
|
$rows[ $id ] = $this->_getRow($child);
|
|
}
|
|
return $rows;
|
|
}
|
|
|
|
/**
|
|
* Determines if the node can be a root-level
|
|
*
|
|
* @param Model $node
|
|
* @return bool
|
|
*/
|
|
public function _canBeRoot( Model $node ) : bool
|
|
{
|
|
return $node instanceof $this->nodeClass;
|
|
}
|
|
|
|
/**
|
|
* Add/Edit Form
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function form() : void
|
|
{
|
|
/* What class are we working with? */
|
|
$nodeClass = $this->nodeClass;
|
|
$parentNodeClass = NULL;
|
|
if ( Request::i()->subnode )
|
|
{
|
|
$parentNodeClass = $nodeClass;
|
|
$nodeClass = $nodeClass::$subnodeClass;
|
|
}
|
|
$node = NULL;
|
|
|
|
/* Init Edit */
|
|
if ( Request::i()->id )
|
|
{
|
|
/* Load the node being edited */
|
|
try
|
|
{
|
|
$node = $nodeClass::load( Request::i()->id );
|
|
Output::i()->title = $node->_title;
|
|
}
|
|
catch ( OutOfRangeException $e )
|
|
{
|
|
Output::i()->error( 'node_error', '2S101/K', 404, '' );
|
|
}
|
|
|
|
/* Check we have permission to edit it */
|
|
if( !$node->canEdit() )
|
|
{
|
|
Output::i()->error( 'node_noperm_edit', '2S101/N', 403, '' );
|
|
}
|
|
}
|
|
|
|
/* Init Create */
|
|
else
|
|
{
|
|
/* Create a new object */
|
|
$node = new $nodeClass;
|
|
|
|
/* Set an appropriate title */
|
|
if ( !$this->title )
|
|
{
|
|
Output::i()->title = Member::loggedIn()->language()->addToStack( $nodeClass::$nodeTitle . '_add_child' );
|
|
}
|
|
else
|
|
{
|
|
Output::i()->title = Member::loggedIn()->language()->addToStack( $this->title );
|
|
}
|
|
|
|
/* Are we creating a child of an existing node? */
|
|
if ( Request::i()->parent )
|
|
{
|
|
$parentColumn = NULL;
|
|
|
|
/* Sub node? */
|
|
if ( Request::i()->subnode )
|
|
{
|
|
if ( isset( $nodeClass::$parentNodeColumnId ) )
|
|
{
|
|
try
|
|
{
|
|
$parent = $parentNodeClass::load( Request::i()->parent );
|
|
if ( !$parent->canAdd() )
|
|
{
|
|
Output::i()->error( 'node_noperm_edit', '2S101/W', 403, '' );
|
|
}
|
|
$parentColumn = $nodeClass::$parentNodeColumnId;
|
|
}
|
|
catch ( OutOfRangeException $e ) { }
|
|
}
|
|
}
|
|
/* Nope, normal */
|
|
elseif ( isset( $nodeClass::$databaseColumnParent ) )
|
|
{
|
|
try
|
|
{
|
|
$parent = $nodeClass::load( Request::i()->parent );
|
|
if ( !$parent->canAdd() )
|
|
{
|
|
Output::i()->error( 'node_noperm_edit', '2S101/V', 403, '' );
|
|
}
|
|
$parentColumn = $nodeClass::$databaseColumnParent;
|
|
}
|
|
catch ( OutOfRangeException $e ) { }
|
|
}
|
|
|
|
/* Set the value */
|
|
if ( $parentColumn !== NULL )
|
|
{
|
|
$node->$parentColumn = Request::i()->parent;
|
|
}
|
|
}
|
|
/* No - creating a root - check permission */
|
|
else
|
|
{
|
|
if( !$nodeClass::canAddRoot() )
|
|
{
|
|
Output::i()->error( 'node_noperm_edit', '2S101/U', 403, '' );
|
|
}
|
|
}
|
|
}
|
|
/* Build form */
|
|
$form = $this->_addEditForm( $node );
|
|
|
|
/* Handle submissions */
|
|
if ( $values = $form->values() )
|
|
{
|
|
if ( isset( Request::i()->massChangeValue ) )
|
|
{
|
|
Output::i()->json( (string) $values[ Request::i()->massChangeValue ] );
|
|
}
|
|
|
|
try
|
|
{
|
|
$new = !$node->_id;
|
|
if ( $new and isset( $node::$databaseColumnOrder ) AND $node::$automaticPositionDetermination === TRUE )
|
|
{
|
|
$orderColumn = $node::$databaseColumnOrder;
|
|
$node->$orderColumn = intval( Db::i()->select( 'MAX(' . $node::$databasePrefix . $orderColumn . ')', $node::$databaseTable )->first() ) + 1;
|
|
}
|
|
|
|
$old = NULL;
|
|
if ( !$new )
|
|
{
|
|
$node->skipCloneDuplication = TRUE;
|
|
$old = clone $node;
|
|
}
|
|
$node->saveForm( $node->formatFormValues( $values ) );
|
|
|
|
if ( $new )
|
|
{
|
|
Session::i()->log( 'acplog__node_created', array( $this->title => TRUE, $node->titleForLog() => FALSE ) );
|
|
|
|
if ( $node->canManagePermissions() )
|
|
{
|
|
Output::i()->redirect( $this->url->setQueryString( array( 'do' => 'permissions', 'id' => $node->_id, 'subnode' => ( isset( Request::i()->subnode ) ? Request::i()->subnode : 0 ) ) ) );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( $node->parent() )
|
|
{
|
|
foreach( $node->parent()->children() AS $child )
|
|
{
|
|
$child->setLastComment();
|
|
$child->setLastReview();
|
|
$child->save();
|
|
}
|
|
|
|
if ( Request::i()->subnode )
|
|
{
|
|
if ( isset( $nodeClass::$parentNodeColumnId ) )
|
|
{
|
|
$parentColumn = $nodeClass::$parentNodeColumnId;
|
|
}
|
|
}
|
|
elseif ( isset( $nodeClass::$databaseColumnParent ) )
|
|
{
|
|
$parentColumn = $nodeClass::$databaseColumnParent;
|
|
}
|
|
|
|
if( isset( $parentColumn ) )
|
|
{
|
|
$node->$parentColumn = $node->parent()->_id;
|
|
}
|
|
|
|
$node->setLastComment();
|
|
$node->setLastReview();
|
|
$node->save();
|
|
}
|
|
|
|
Session::i()->log( 'acplog__node_edited', array( $this->title => TRUE, $node->titleForLog() => FALSE ) );
|
|
}
|
|
|
|
$this->_afterSave( $old, $node, $form->getLastUsedTab() );
|
|
return;
|
|
}
|
|
catch ( LogicException $e )
|
|
{
|
|
$form->error = $e->getMessage();
|
|
}
|
|
}
|
|
|
|
/* Display */
|
|
Output::i()->output .= $this->_showForm( $form );
|
|
}
|
|
|
|
/**
|
|
* Get form
|
|
*
|
|
* @param Model $node
|
|
* @return Form
|
|
*/
|
|
protected function _addEditForm( Model $node ): Form
|
|
{
|
|
$form = new Form( 'form_' . ( $node->_id ?: 'new' ) );
|
|
if ( $node->_id AND !$node->noCopyButton )
|
|
{
|
|
$form->copyButton = $this->url->setQueryString( array( 'do' => 'massChange', 'from' => $node->_id ) );
|
|
if ( !( $node instanceof $this->nodeClass ) )
|
|
{
|
|
$form->copyButton = $form->copyButton->setQueryString( 'subnode', 1 );
|
|
}
|
|
}
|
|
|
|
$node->form($form);
|
|
|
|
return $form;
|
|
}
|
|
|
|
/**
|
|
* Get form
|
|
*
|
|
* @param Form $form The form as returned by _addEditForm()
|
|
* @return string
|
|
*/
|
|
protected function _showForm( Form $form ): string
|
|
{
|
|
return (string) $form;
|
|
}
|
|
|
|
/**
|
|
* Redirect after save
|
|
*
|
|
* @param Model|null $old A clone of the node as it was before or NULL if this is a creation
|
|
* @param Model $new The node now
|
|
* @param bool|string $lastUsedTab The tab last used in the form
|
|
* @return void
|
|
*/
|
|
protected function _afterSave( ?Model $old, Model $new, bool|string $lastUsedTab = FALSE ): void
|
|
{
|
|
if( Request::i()->isAjax() )
|
|
{
|
|
Output::i()->json( array() );
|
|
}
|
|
else
|
|
{
|
|
if( isset( Request::i()->save_and_reload ) )
|
|
{
|
|
$buttons = $new->getButtons($this->url, !($new instanceof $this->nodeClass));
|
|
|
|
Output::i()->redirect( ( $lastUsedTab ? $buttons['edit']['link']->setQueryString('activeTab', $lastUsedTab ) : $buttons['edit']['link'] ), 'saved' );
|
|
}
|
|
else
|
|
{
|
|
Output::i()->redirect( $this->url->setQueryString( array( 'root' => ( $new->parent() ? $new->parent()->_id : '' ) ) ), 'saved' );
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mass Change
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function massChange() : void
|
|
{
|
|
/* Check permission */
|
|
$nodeClass = $this->nodeClass;
|
|
if ( Request::i()->subnode )
|
|
{
|
|
$nodeClass = $nodeClass::$subnodeClass;
|
|
}
|
|
try
|
|
{
|
|
$node = $nodeClass::load( Request::i()->from );
|
|
}
|
|
catch ( OutOfRangeException $e )
|
|
{
|
|
Output::i()->error( 'node_error', '2S101/S', 404, '' );
|
|
}
|
|
if( !$node->canEdit() )
|
|
{
|
|
Output::i()->error( 'node_noperm_edit', '2S101/T', 403, '' );
|
|
}
|
|
|
|
/* Get the value */
|
|
$key = Request::i()->key;
|
|
$sessionKey = 'MC' . $key;
|
|
$dummyForm = new Form;
|
|
$node->form( $dummyForm );
|
|
|
|
if( isset( Request::i()->value ) )
|
|
{
|
|
$_SESSION[ $sessionKey ] = Request::i()->value;
|
|
}
|
|
|
|
if( isset( $_SESSION[ $sessionKey ] ) )
|
|
{
|
|
$value = $_SESSION[ $sessionKey ];
|
|
}
|
|
else
|
|
{
|
|
$value = isset( Request::i()->value ) ? Request::i()->value : NULL;
|
|
if ( $value === NULL )
|
|
{
|
|
foreach ( $dummyForm->elements as $tab => $elements )
|
|
{
|
|
if ( isset( $elements[ $key ] ) )
|
|
{
|
|
$value = $elements[ $key ]->value;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
$_SESSION[ $sessionKey ] = $value;
|
|
}
|
|
|
|
/* Build Form */
|
|
$form = new Form;
|
|
$form->ajaxOutput = TRUE;
|
|
$field = new FormNode( 'nodes', array(), TRUE, array( 'url' => $this->url->setQueryString( array( 'do' => 'massChange', 'key' => $key, 'from' => $node->_id ) ), 'class' => $nodeClass, 'zeroVal' => 'all', 'multiple' => TRUE, 'permissionCheck' => function( $node ) use ( $key, $value )
|
|
{
|
|
return $node->canCopyValue( $key, $value );
|
|
} ) );
|
|
$field->label = Member::loggedIn()->language()->get( 'copy_value_to' );
|
|
$form->add( $field );
|
|
|
|
/* Display */
|
|
if ( $values = $form->values() )
|
|
{
|
|
$url = $this->url;
|
|
$multiRedirectUrl = $this->url->setQueryString( array( 'do' => 'massChange', 'key' => Request::i()->key, 'nodes' => Request::i()->nodes ?: $values['nodes'], 'from' => Request::i()->from, 'form_submitted' => 1, 'csrfKey' => Request::i()->csrfKey ) );
|
|
if ( Request::i()->subnode )
|
|
{
|
|
$multiRedirectUrl = $multiRedirectUrl->setQueryString( 'subnode', 1 );
|
|
}
|
|
Output::i()->output = new MultipleRedirect( $multiRedirectUrl,
|
|
function( $doneSoFar ) use ( $nodeClass, $sessionKey )
|
|
{
|
|
$count = Db::i()->select( 'COUNT(*)', $nodeClass::$databaseTable, Request::i()->nodes == 0 ? NULL : Db::i()->in( $nodeClass::$databasePrefix . $nodeClass::$databaseColumnId, explode( ',', Request::i()->nodes) ) )->first();
|
|
if ( !$count )
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
$select = Db::i()->select( '*', $nodeClass::$databaseTable, Request::i()->nodes == 0 ? NULL : Db::i()->in( $nodeClass::$databasePrefix . $nodeClass::$databaseColumnId, explode( ',', Request::i()->nodes) ), $nodeClass::$databasePrefix . $nodeClass::$databaseColumnId, array( $doneSoFar, 50 ) );
|
|
|
|
$did = 0;
|
|
foreach ( $select as $row )
|
|
{
|
|
$did++;
|
|
$_node = $nodeClass::constructFromData( $row );
|
|
|
|
if ( $_node->canCopyValue( Request::i()->key, $_SESSION[ $sessionKey ] ) )
|
|
{
|
|
$values = $_node->formatFormValues( array( Request::i()->key => $_SESSION[ $sessionKey ] ) );
|
|
|
|
foreach( $values as $k => $v )
|
|
{
|
|
$k = preg_replace( '#^' . preg_quote( $nodeClass::$databasePrefix, '#' ) . '#', "", $k );
|
|
$val = $_node->$k;
|
|
|
|
if( is_array( $v ) )
|
|
{
|
|
foreach( $v as $_k => $_v )
|
|
{
|
|
$val[ $_k ] = $_v;
|
|
}
|
|
|
|
$_node->$k = $val;
|
|
}
|
|
else
|
|
{
|
|
$_node->$k = $v;
|
|
}
|
|
}
|
|
|
|
$_node->save();
|
|
}
|
|
}
|
|
|
|
if( !$did )
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
$doneSoFar += 50;
|
|
return array( $doneSoFar, Member::loggedIn()->language()->addToStack('copying'), 100 / $count * $doneSoFar );
|
|
},
|
|
function() use( $url, $sessionKey )
|
|
{
|
|
/* Clear the value in the session */
|
|
unset( $_SESSION[ $sessionKey ] );
|
|
|
|
/* And redirect */
|
|
$finishUrl = $url->setQueryString( array( 'do' => 'form', 'id' => Request::i()->from ) );
|
|
if ( Request::i()->subnode )
|
|
{
|
|
$finishUrl = $finishUrl->setQueryString( 'subnode', 1 );
|
|
}
|
|
Output::i()->redirect( $finishUrl );
|
|
},
|
|
FALSE
|
|
);
|
|
}
|
|
else
|
|
{
|
|
Output::i()->output = Theme::i()->getTemplate('global', 'core')->block( '', $form );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Toggle Enabled/Disable
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function enableToggle() : void
|
|
{
|
|
Session::i()->csrfCheck();
|
|
|
|
/* Work out which class we're using */
|
|
$nodeClass = $this->nodeClass;
|
|
if ( mb_substr( Request::i()->id, 0, 2 ) === 's.' )
|
|
{
|
|
Request::i()->id = mb_substr( Request::i()->id, 2 );
|
|
$nodeClass = $nodeClass::$subnodeClass;
|
|
}
|
|
|
|
/* Load Node */
|
|
try
|
|
{
|
|
$node = $nodeClass::load( Request::i()->id );
|
|
}
|
|
catch ( OutOfRangeException $e )
|
|
{
|
|
Output::i()->error( 'node_error', '3S101/A', 404, '' );
|
|
}
|
|
|
|
/* Check we're not locked */
|
|
if( $node->_locked or !$node->canEdit() )
|
|
{
|
|
Output::i()->error( 'node_noperm_enable', '2S101/3', 403, '' );
|
|
}
|
|
|
|
/* Toggle */
|
|
$node->_enabled = Request::i()->status;
|
|
$node->save();
|
|
|
|
/* Recount if needed */
|
|
if( $node->parent() )
|
|
{
|
|
$node->parent()->setLastComment();
|
|
$node->parent()->setLastReview();
|
|
$node->parent()->save();
|
|
}
|
|
|
|
$this->_logToggleAndRedirect( $node );
|
|
}
|
|
|
|
/**
|
|
* Following a toggle, log the action and redirect. Abstracted so it can be called separately externally.
|
|
*
|
|
* @param Model $node The node we are working with
|
|
* @return void
|
|
*/
|
|
public function _logToggleAndRedirect( Model $node ) : void
|
|
{
|
|
/* Log */
|
|
if ( $node->_enabled )
|
|
{
|
|
Session::i()->log( 'acplog__node_enabled', array( $this->title => TRUE, $node->titleForLog() => FALSE ) );
|
|
}
|
|
else
|
|
{
|
|
Session::i()->log( 'acplog__node_disabled', array( $this->title => TRUE, $node->titleForLog() => FALSE ) );
|
|
}
|
|
|
|
/* If this is an AJAX request, just respond */
|
|
if( Request::i()->isAjax() )
|
|
{
|
|
Output::i()->json( $node->_enabled );
|
|
}
|
|
/* Otherwise, redirect */
|
|
else
|
|
{
|
|
Output::i()->redirect( $this->url->setQueryString( array( 'root' => Request::i()->root ) ) );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Copy
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function copy() : void
|
|
{
|
|
/* Get node */
|
|
$nodeClass = $this->nodeClass;
|
|
if ( Request::i()->subnode )
|
|
{
|
|
$nodeClass = $nodeClass::$subnodeClass;
|
|
}
|
|
try
|
|
{
|
|
$node = $nodeClass::load( Request::i()->id );
|
|
}
|
|
catch ( OutOfRangeException $e )
|
|
{
|
|
Output::i()->error( 'node_error', '2S101/L', 404, '' );
|
|
}
|
|
|
|
/* Do we have any children? */
|
|
if ( $node->hasChildren( NULL, NULL, FALSE ) and !isset( Request::i()->skipChildren ) )
|
|
{
|
|
$form = new Form;
|
|
$form->add( new YesNo( 'node_copy_children', NULL, FALSE, array(), function( $val ) use ( &$form )
|
|
{
|
|
if ( $val )
|
|
{
|
|
$form->ajaxOutput = TRUE;
|
|
}
|
|
} ) );
|
|
|
|
/* A multiredirect has been initiated so we need to let the code flow to the MR helper below, but check CSRF key first */
|
|
if( Request::i()->node_copy_children )
|
|
{
|
|
Session::i()->csrfCheck();
|
|
}
|
|
|
|
if ( $values = $form->values() OR Request::i()->node_copy_children )
|
|
{
|
|
/* Copy Children */
|
|
if ( $values['node_copy_children'] OR Request::i()->node_copy_children )
|
|
{
|
|
$multipleRedirect = new MultipleRedirect(
|
|
$this->url->setQueryString( array( 'do' => 'copy', 'id' => $node->_id, 'subnode' => Request::i()->subnode, 'form_submitted' => 1, 'csrfKey' => Request::i()->csrfKey, 'node_copy_children' => 1 ) ),
|
|
/* Process */
|
|
function( $data ) use ( $node, $nodeClass )
|
|
{
|
|
/* Init */
|
|
if ( !is_array( $data ) )
|
|
{
|
|
return array( array( 'copy' => array( array( 'id' => $node->_id, 'subnode' => 0 ) ), 'ids' => array() ), Member::loggedIn()->language()->addToStack('copying') );
|
|
}
|
|
/* Process */
|
|
else
|
|
{
|
|
/* Have we finished? */
|
|
if ( empty( $data['copy'] ) )
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
/* No, still going */
|
|
foreach( $data['copy'] as $k => $itemData )
|
|
{
|
|
/* Load */
|
|
if ( $itemData['subnode'] )
|
|
{
|
|
$nodeClass = $nodeClass::$subnodeClass;
|
|
}
|
|
$item = $nodeClass::load( $itemData['id'] );
|
|
|
|
/* Copy it */
|
|
$new = clone $item;
|
|
$data['ids'][ get_class( $item ) ][ $item->_id ] = $new->_id;
|
|
|
|
/* Update it's parent */
|
|
if ( $item->parent() )
|
|
{
|
|
if ( array_key_exists( $item->parent()->_id, $data['ids'][ get_class( $item->parent() ) ] ) )
|
|
{
|
|
$parentColumn = $nodeClass::$databaseColumnParent;
|
|
if ( $itemData['subnode'] )
|
|
{
|
|
$parentColumn = $nodeClass::$parentNodeColumnId;
|
|
}
|
|
|
|
$new->$parentColumn = $data['ids'][ get_class( $item->parent() ) ][ $item->parent()->_id ];
|
|
$new->save();
|
|
}
|
|
}
|
|
|
|
/* Remove this one from our array */
|
|
unset( $data['copy'][ $k ] );
|
|
|
|
/* And add all it's children */
|
|
foreach ( $item->children( NULL ) as $child )
|
|
{
|
|
$data['copy'][] = array( 'id' => $child->_id, 'subnode' => !( $child instanceof $nodeClass ) );
|
|
}
|
|
|
|
/* Return */
|
|
return array( $data, Member::loggedIn()->language()->addToStack('copying') );
|
|
}
|
|
}
|
|
|
|
return [];
|
|
},
|
|
/* Finish */
|
|
function() use ( $node )
|
|
{
|
|
Session::i()->log( 'acplog__node_copied_c', array( $node->title => TRUE, $node->titleForLog() => FALSE ) );
|
|
Output::i()->redirect( $this->url->setQueryString( array( 'root' => ( $node->parent() ? $node->parent()->id : '' ) ) ), 'saved' );
|
|
}
|
|
);
|
|
Output::i()->output = $multipleRedirect;
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* Show form */
|
|
Output::i()->output = $form;
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Session::i()->csrfCheck();
|
|
}
|
|
|
|
/* Copy it */
|
|
$new = clone $node;
|
|
Session::i()->log( 'acplog__node_copied', array( $this->title => TRUE, $node->titleForLog() => FALSE ) );
|
|
|
|
/* Boink */
|
|
$url = $this->url->setQueryString( array( 'do' => 'form', 'id' => $new->_id ) );
|
|
if ( isset( Request::i()->subnode ) )
|
|
{
|
|
$url = $url->setQueryString( 'subnode', 1 );
|
|
}
|
|
Output::i()->redirect( $url, 'copied' );
|
|
}
|
|
|
|
/**
|
|
* Reorder
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function reorder() : void
|
|
{
|
|
Session::i()->csrfCheck();
|
|
|
|
/* Init */
|
|
$nodeClass = $this->nodeClass;
|
|
|
|
/* Normalise AJAX vs non-AJAX */
|
|
if( isset( Request::i()->ajax_order ) )
|
|
{
|
|
$order = array();
|
|
$position = array();
|
|
foreach( Request::i()->ajax_order as $id => $parent )
|
|
{
|
|
if ( !isset( $order[ $parent ] ) )
|
|
{
|
|
$order[ $parent ] = array();
|
|
$position[ $parent ] = 1;
|
|
}
|
|
$order[ $parent ][ $id ] = $position[ $parent ]++;
|
|
}
|
|
}
|
|
/* Non-AJAX way */
|
|
else
|
|
{
|
|
$order = array( Request::i()->root ?: 'null' => Request::i()->order );
|
|
}
|
|
|
|
/* Okay, now order */
|
|
foreach( $order as $parent => $nodes )
|
|
{
|
|
foreach ( $nodes as $id => $position )
|
|
{
|
|
/* Load Node */
|
|
try
|
|
{
|
|
if ( mb_substr( $id, 0, 2 ) === 's.' )
|
|
{
|
|
$subnodeClass = $nodeClass::$subnodeClass;
|
|
|
|
$node = $subnodeClass::load( mb_substr( $id, 2 ) );
|
|
$parentColumn = $node::$parentNodeColumnId;
|
|
}
|
|
else
|
|
{
|
|
$node = $nodeClass::load( $id );
|
|
$parentColumn = $node::$databaseColumnParent;
|
|
}
|
|
}
|
|
catch ( OutOfRangeException $e )
|
|
{
|
|
Output::i()->error( 'node_error', '3S101/B', 404, '' );
|
|
}
|
|
$orderColumn = $node::$databaseColumnOrder;
|
|
$idColumn = $node::$databaseColumnId;
|
|
|
|
/* Check permission */
|
|
if( !$node->canEdit() )
|
|
{
|
|
continue;
|
|
}
|
|
if( !$node::$nodeSortable or $orderColumn === NULL )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
/* Do it */
|
|
if ( $parentColumn )
|
|
{
|
|
if ( is_numeric( $parent ) and $node->$idColumn == $parent AND !isset( $nodeClass::$subnodeClass ) )
|
|
{
|
|
/* It is attempting to assign a parent ID of itself which will break the tree */
|
|
$parent = $nodeClass::$databaseColumnParentRootValue;
|
|
}
|
|
|
|
$node->$parentColumn = ( $parent === 'null' ) ? $nodeClass::$databaseColumnParentRootValue : ( is_numeric( $parent ) ? $parent : $nodeClass::$databaseColumnParentRootValue );
|
|
}
|
|
|
|
$node->$orderColumn = $position;
|
|
$node->save();
|
|
}
|
|
|
|
if( $parent !== 'null' )
|
|
{
|
|
$node = $nodeClass::load( $parent );
|
|
$node->setLastComment();
|
|
$node->setLastReview();
|
|
$node->save();
|
|
}
|
|
}
|
|
|
|
/* Log */
|
|
Session::i()->log( 'acplog__node_reorder', array( $this->title => TRUE ), TRUE );
|
|
|
|
/* Allow plugins to act if necessary */
|
|
$this->_afterReorder( $order );
|
|
|
|
/* If this is an AJAX request, just respond */
|
|
if( Request::i()->isAjax() )
|
|
{
|
|
return;
|
|
}
|
|
/* Otherwise, redirect */
|
|
else
|
|
{
|
|
Output::i()->redirect( $this->url->setQueryString( array( 'root' => Request::i()->root ) ) );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Function to execute after nodes are reordered. Do nothing by default but plugins can extend.
|
|
*
|
|
* @param array $order The new ordering that was saved
|
|
* @return void
|
|
*/
|
|
protected function _afterReorder( array $order ) : void
|
|
{
|
|
|
|
}
|
|
|
|
/**
|
|
* Permissions
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function permissions() : void
|
|
{
|
|
/* Get node */
|
|
$nodeClass = $this->nodeClass;
|
|
if ( Request::i()->subnode )
|
|
{
|
|
$nodeClass = $nodeClass::$subnodeClass;
|
|
}
|
|
$node = NULL;
|
|
|
|
if ( Request::i()->id )
|
|
{
|
|
try
|
|
{
|
|
$node = $nodeClass::load( Request::i()->id );
|
|
Output::i()->title = $node->_title;
|
|
}
|
|
catch ( OutOfRangeException $e )
|
|
{
|
|
Output::i()->error( 'node_error', '2S101/M', 404, '' );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Output::i()->error( 'node_error', '2S101/X', 404, '' );
|
|
}
|
|
|
|
/* Check permission */
|
|
if( !$node->canManagePermissions() )
|
|
{
|
|
Output::i()->error( 'node_noperm_edit', '2S101/O', 403, '' );
|
|
}
|
|
|
|
/* Get current permissions */
|
|
try
|
|
{
|
|
$current = Db::i()->select( '*', 'core_permission_index', array( 'app=? AND perm_type=? AND perm_type_id=?', $nodeClass::$permApp, $nodeClass::$permType, $node->_id ) )->first();
|
|
}
|
|
catch( UnderflowException $e )
|
|
{
|
|
/* Recommended permissions */
|
|
$current = array();
|
|
foreach ( $nodeClass::$permissionMap as $k => $v )
|
|
{
|
|
switch ( $k )
|
|
{
|
|
case 'view':
|
|
case 'read':
|
|
$current["perm_{$v}"] = '*';
|
|
break;
|
|
|
|
case 'add':
|
|
case 'reply':
|
|
case 'review':
|
|
case 'upload':
|
|
case 'download':
|
|
default:
|
|
$current["perm_{$v}"] = implode( ',', array_keys( Group::groups( TRUE, FALSE ) ) );
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Build Matrix */
|
|
$matrix = new Matrix;
|
|
$matrix->manageable = FALSE;
|
|
$matrix->langPrefix = $nodeClass::$permissionLangPrefix . 'perm__';
|
|
$matrix->styledRowTitle = TRUE;
|
|
$matrix->columns = array(
|
|
'label' => function( $key, $value, $data )
|
|
{
|
|
$groupId = explode( '[', $key )[0];
|
|
$groupLink = NULL;
|
|
|
|
/* Let's try to get a link to the group's permissions */
|
|
try
|
|
{
|
|
Group::load( $groupId );
|
|
$groupLink = Url::internal( "app=core&module=members&controller=groups&do=permissions&id=" . $groupId );
|
|
}
|
|
catch ( OutOfRangeException $e ) { }
|
|
|
|
return Theme::i()->getTemplate( 'global', 'core', 'global' )->titleWithLink( $value, $groupLink, 'all_permissions', Member::loggedIn()->language()->addToStack( "all_permissions_group_hovertitle", FALSE, array( 'htmlsprintf' => [ $value ] ) ) );
|
|
},
|
|
);
|
|
|
|
try
|
|
{
|
|
$disabledPermissions = $node->disabledPermissions();
|
|
}
|
|
catch( UnderflowException $e )
|
|
{
|
|
if( $e->getCode() != 199 )
|
|
{
|
|
throw $e;
|
|
}
|
|
|
|
Output::i()->error( 'generic_error', '4F383/1', 500, $e->getMessage() );
|
|
}
|
|
|
|
foreach ( $node->permissionTypes() as $k => $v )
|
|
{
|
|
$matrix->columns[ $k ] = function( $key, $value, $data ) use ( $current, $k, $v, $disabledPermissions )
|
|
{
|
|
$groupId = mb_substr( $key, 0, -( 2 + mb_strlen( $k ) ) );
|
|
$disabled = FALSE;
|
|
|
|
if ( array_key_exists( $groupId, $disabledPermissions ) and is_array( $disabledPermissions[ $groupId ] ) )
|
|
{
|
|
$disabled = in_array( $v, array_values( $disabledPermissions[ $groupId ] ) );
|
|
}
|
|
|
|
if ( $disabled === FALSE )
|
|
{
|
|
$disabled = ( $groupId == Settings::i()->guest_group AND in_array( $k, array( 'review', 'rate' ) ) );
|
|
}
|
|
|
|
$fieldValue = ( isset( $current[ "perm_{$v}" ] ) and ( $current[ "perm_{$v}" ] === '*' or in_array( $groupId, explode( ',', $current[ "perm_{$v}" ] ) ) ) );
|
|
|
|
return new Checkbox( $key, ( $disabled ? 0 : $fieldValue ), NULL, array( 'disabled' => $disabled ) );
|
|
};
|
|
$matrix->checkAlls[ $k ] = ( $current[ "perm_{$v}" ] === '*' );
|
|
}
|
|
$matrix->checkAllRows = TRUE;
|
|
|
|
$rows = array();
|
|
foreach ( Group::groups() as $group )
|
|
{
|
|
$rows[ $group->g_id ] = array(
|
|
'label' => $group->name,
|
|
'view' => TRUE,
|
|
);
|
|
}
|
|
$matrix->rows = $rows;
|
|
|
|
/* Handle submissions */
|
|
if ( $values = $matrix->values() )
|
|
{
|
|
$_perms = array();
|
|
|
|
/* Check for "all" checkboxes */
|
|
foreach ( $nodeClass::$permissionMap as $k => $v )
|
|
{
|
|
if ( isset( Request::i()->__all[ $k ] ) )
|
|
{
|
|
$shouldCheckAll = TRUE;
|
|
foreach( $matrix->elements as $group => $element )
|
|
{
|
|
if ( isset( $element[ $k ] ) and $element[ $k ]->options['disabled'] )
|
|
{
|
|
$shouldCheckAll = FALSE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
$_perms[ $v ] = $shouldCheckAll ? '*' : array();
|
|
}
|
|
else
|
|
{
|
|
$_perms[ $v ] = array();
|
|
}
|
|
}
|
|
|
|
/* Prepare insert */
|
|
$insert = array( 'app' => $nodeClass::$permApp, 'perm_type' => $nodeClass::$permType, 'perm_type_id' => $node->_id );
|
|
if ( isset( $current['perm_id'] ) )
|
|
{
|
|
$insert['perm_id'] = $current['perm_id'];
|
|
}
|
|
|
|
/* Loop groups */
|
|
foreach ( $values as $group => $perms )
|
|
{
|
|
foreach ( $nodeClass::$permissionMap as $k => $v )
|
|
{
|
|
if ( isset( $perms[ $k ] ) and $perms[ $k ] and is_array( $_perms[ $v ] ) )
|
|
{
|
|
$_perms[ $v ][] = $group;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Finalise */
|
|
foreach ( $_perms as $k => $v )
|
|
{
|
|
$insert[ "perm_{$k}" ] = is_array( $v ) ? implode( ',', $v ) : $v;
|
|
}
|
|
|
|
/* Set the permissions */
|
|
$node->setPermissions( $insert );
|
|
|
|
unset(Store::i()->modules);
|
|
|
|
/* Log */
|
|
Session::i()->log( 'permissions_adjusted_node', array( $node->titleForLog() => FALSE ) );
|
|
|
|
/* Redirect */
|
|
$this->_afterSave( NULL, $node );
|
|
return;
|
|
}
|
|
|
|
/* Display */
|
|
Output::i()->output .= $matrix;
|
|
}
|
|
|
|
/**
|
|
* Delete
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function delete() : void
|
|
{
|
|
/* Get node */
|
|
$nodeClass = $this->nodeClass;
|
|
if ( Request::i()->subnode )
|
|
{
|
|
$nodeClass = $nodeClass::$subnodeClass;
|
|
}
|
|
|
|
try
|
|
{
|
|
$node = $nodeClass::load( Request::i()->id );
|
|
}
|
|
catch ( OutOfRangeException $e )
|
|
{
|
|
Output::i()->error( 'node_error', '2S101/J', 404, '' );
|
|
}
|
|
|
|
/* Permission check */
|
|
if( !$node->canDelete() )
|
|
{
|
|
Output::i()->error( 'node_noperm_delete', '2S101/H', 403, '' );
|
|
}
|
|
|
|
/* Do we have any children or content? */
|
|
if ( $node->hasChildren( NULL, NULL, TRUE ) or $node->showDeleteOrMoveForm() or isset( Request::i()->ajaxValidate ) )
|
|
{
|
|
$form = $node->deleteOrMoveForm( FALSE );
|
|
if ( $values = $form->values() )
|
|
{
|
|
$node->deleteOrMoveFormSubmit( $values );
|
|
|
|
if ( isset( $values['node_move_children'] ) AND $values['node_move_children'] )
|
|
{
|
|
$moveToId = ( isset( $values['node_destination'] ) ) ? $values['node_destination'] : Request::i()->node_move_children;
|
|
|
|
Session::i()->log( 'acplog__node_deleted_m', array( $this->title => TRUE, $node->titleForLog() => FALSE, $node::load( $moveToId )->titleForLog() => FALSE ) );
|
|
}
|
|
else
|
|
{
|
|
Session::i()->log( 'acplog__node_deleted_c', array( $this->title => TRUE, $node->titleForLog() => FALSE ) );
|
|
}
|
|
|
|
Output::i()->redirect( $this->url->setQueryString( array( 'root' => ( $node->parent() ? $node->parent()->_id : '' ) ) ), 'deleted' );
|
|
}
|
|
else
|
|
{
|
|
/* Show form */
|
|
Output::i()->output = $form;
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* Make sure the user confirmed the deletion */
|
|
Request::i()->confirmedDelete();
|
|
}
|
|
|
|
/* Delete it */
|
|
Session::i()->log( 'acplog__node_deleted', array( $this->title => TRUE, $node->titleForLog() => FALSE ) );
|
|
$node->delete();
|
|
|
|
/* Boink */
|
|
if( Request::i()->isAjax() )
|
|
{
|
|
Output::i()->json( "OK" );
|
|
}
|
|
else
|
|
{
|
|
Output::i()->redirect( $this->url->setQueryString( array( 'root' => ( $node->parent() ? $node->parent()->_id : '' ) ) ), 'deleted' );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mass Move/Delete Content
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function massManageContent() : void
|
|
{
|
|
/* Get node */
|
|
$nodeClass = $this->nodeClass;
|
|
if ( Request::i()->subnode )
|
|
{
|
|
$nodeClass = $nodeClass::$subnodeClass;
|
|
}
|
|
try
|
|
{
|
|
$node = $nodeClass::load( Request::i()->id );
|
|
}
|
|
catch ( OutOfRangeException $e )
|
|
{
|
|
Output::i()->error( 'node_error', '2S101/X', 404, '' );
|
|
}
|
|
|
|
/* Permission check */
|
|
if( !$node->canMassManageContent() )
|
|
{
|
|
Output::i()->error( 'node_noperm_delete', '2S101/Y', 403, '' );
|
|
}
|
|
|
|
/* Is there any content? */
|
|
if ( !isset( $nodeClass::$contentItemClass ) or !$node->getContentItemCount() )
|
|
{
|
|
Output::i()->error( 'node_mass_content_none', '2S101/Z' );
|
|
}
|
|
$contentItemClass = $nodeClass::$contentItemClass;
|
|
|
|
/* Build Wizard */
|
|
Output::i()->title = $node->_title;
|
|
Output::i()->output = (string) new Wizard(
|
|
array(
|
|
'node_mass_content_form' => function( $data ) use( $nodeClass, $node, $contentItemClass )
|
|
{
|
|
/* Fetch the form to mass move content - abstracted so other apps can adjust if necessary */
|
|
$form = new Form( 'delete_node_form', 'continue', $this->url->setQueryString( array( 'do' => 'massManageContent', 'id' => $node->_id, 'subnode' => Request::i()->subnode, '_step' => 'node_mass_content_form' ) ) );
|
|
$form = $this->_buildMassMoveForm( $form, $data, $nodeClass, $node, $contentItemClass );
|
|
|
|
/* Handle submissions */
|
|
if ( $values = $form->values() )
|
|
{
|
|
return $this->_processMassMoveForm( $values, $data, $nodeClass, $node, $contentItemClass );
|
|
}
|
|
|
|
/* Display Form */
|
|
return (string) $form;
|
|
},
|
|
'node_mass_content_confirm' => function( $data ) use ( $node, $contentItemClass )
|
|
{
|
|
return $this->_performMassMove( $data, $node, $contentItemClass );
|
|
}
|
|
),
|
|
$this->url->setQueryString( array( 'do' => 'massManageContent', 'id' => $node->_id, 'subnode' => Request::i()->subnode ) )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Build the form to mass move content
|
|
*
|
|
* @param Form $form The form helper object
|
|
* @param mixed $data Data from the wizard helper
|
|
* @param string $nodeClass Node class
|
|
* @param Model $node Node we are working with
|
|
* @param string $contentItemClass Content item class (if there is one)
|
|
* @return Form
|
|
*/
|
|
protected function _buildMassMoveForm( Form $form, mixed $data, string $nodeClass, Model $node, string $contentItemClass ): Form
|
|
{
|
|
$form->addHeader('node_mass_move_delete_if');
|
|
$form->add( new Form\Member( 'node_move_author', isset( $data['additional']['author'] ) ? array_map( function($id ) {
|
|
return Member::load( $id );
|
|
}, $data['additional']['author'] ) : NULL, FALSE, array( 'nullLang' => 'node_mass_move_anyone', 'multiple' => NULL ) ) );
|
|
$form->add( new Date( 'node_move_date', $data['additional']['date'] ?? 0, FALSE, array( 'unlimited' => 0, 'unlimitedLang' => 'node_mass_move_any_time' ) ) );
|
|
if ( isset( $contentItemClass::$commentClass ) )
|
|
{
|
|
$form->add( new Number( 'node_move_comments', $data['additional']['num_comments'] ?? -1, FALSE, array( 'unlimited' => -1 ), NULL, NULL, NULL, 'node_move_comments_less' ) );
|
|
$form->add( new Date( 'node_move_last_post', $data['additional']['last_post'] ?? 0, FALSE, array( 'unlimited' => 0, 'unlimitedLang' => 'node_mass_move_any_time' ) ) );
|
|
}
|
|
if ( IPS::classUsesTrait( $contentItemClass, 'IPS\Content\Lockable' ) )
|
|
{
|
|
$form->add( new Radio( 'node_move_state', $data['additional']['state'] ?? 'any', FALSE, array( 'options' => array( 'locked' => 'locked', 'open' => 'unlocked', 'any' => 'node_mass_move_either' ) ) ) );
|
|
}
|
|
if ( IPS::classUsesTrait( $contentItemClass, 'IPS\Content\Pinnable' ) )
|
|
{
|
|
$form->add( new Radio( 'node_move_pinned', $data['additional']['pinned'] ?? 'any', FALSE, array( 'options' => array( '1' => 'pinned', '0' => 'node_move_pinned_not_pinned', 'any' => 'node_mass_move_either' ) ) ) );
|
|
}
|
|
if ( IPS::classUsesTrait( $contentItemClass, 'IPS\Content\Featurable' ) )
|
|
{
|
|
$form->add( new Radio( 'node_move_featured', $data['additional']['featured'] ?? 'any', FALSE, array( 'options' => array( '1' => 'featured', '0' => 'node_move_pinned_not_featured', 'any' => 'node_mass_move_either' ) ) ) );
|
|
}
|
|
$form->addHeader('node_mass_move_delete_then');
|
|
$moveToClass = $data['moveToClass'] ?? $nodeClass;
|
|
$form->add( new FormNode( 'node_move_content', isset( $data['moveTo'] ) ? $moveToClass::load( $data['moveTo'] ) : 0, TRUE, array( 'class' => $nodeClass, 'disabledIds' => array( $node->_id ), 'disabledLang' => 'node_move_delete', 'zeroVal' => 'node_delete_content', 'subnodes' => FALSE, 'permissionCheck' => function( $node )
|
|
{
|
|
return array_key_exists( 'add', $node->permissionTypes() );
|
|
} ) ) );
|
|
|
|
return $form;
|
|
}
|
|
|
|
/**
|
|
* Process the mass move form submission
|
|
*
|
|
* @param array $values Values from form submission
|
|
* @param mixed $data Data from the wizard helper
|
|
* @param string $nodeClass Node class
|
|
* @param Model $node Node we are working with
|
|
* @param string $contentItemClass Content item class (if there is one)
|
|
* @return array Wizard helper data
|
|
*/
|
|
protected function _processMassMoveForm(array $values, mixed $data, string $nodeClass, Model $node, string $contentItemClass ): array
|
|
{
|
|
$data['deleteWhenDone'] = FALSE;
|
|
$data['class'] = get_class( $node );
|
|
$data['id'] = $node->_id;
|
|
|
|
if ( is_object( $values['node_move_content'] ) )
|
|
{
|
|
$data['moveToClass'] = get_class( $values['node_move_content'] );
|
|
$data['moveTo'] = $values['node_move_content']->_id;
|
|
}
|
|
else
|
|
{
|
|
unset( $data['moveToClass'] );
|
|
unset( $data['moveTo'] );
|
|
}
|
|
|
|
if ( count( $values['node_move_author'] ) )
|
|
{
|
|
$data['additional']['author'] = array_keys( $values['node_move_author'] );
|
|
}
|
|
else
|
|
{
|
|
unset( $data['additional']['author'] );
|
|
}
|
|
|
|
if ( $values['node_move_date'] )
|
|
{
|
|
$data['additional']['date'] = $values['node_move_date']->setTime( 23, 59, 59 )->getTimestamp();
|
|
}
|
|
else
|
|
{
|
|
unset( $data['additional']['date'] );
|
|
}
|
|
|
|
if ( isset( $values['node_move_comments'] ) and $values['node_move_comments'] > -1 )
|
|
{
|
|
$data['additional']['num_comments'] = $values['node_move_comments'];
|
|
}
|
|
else
|
|
{
|
|
unset( $data['additional']['num_comments'] );
|
|
}
|
|
|
|
if ( isset( $values['node_move_last_post'] ) and $values['node_move_last_post'] )
|
|
{
|
|
$data['additional']['last_post'] = $values['node_move_last_post']->setTime( 23, 59, 59 )->getTimestamp();
|
|
}
|
|
else
|
|
{
|
|
unset( $data['additional']['last_post'] );
|
|
}
|
|
|
|
if ( isset( $values['node_move_state'] ) and $values['node_move_state'] != 'any' )
|
|
{
|
|
$data['additional']['state'] = $values['node_move_state'];
|
|
}
|
|
else
|
|
{
|
|
unset( $data['additional']['state'] );
|
|
}
|
|
|
|
if ( isset( $values['node_move_pinned'] ) and $values['node_move_pinned'] != 'any' )
|
|
{
|
|
$data['additional']['pinned'] = $values['node_move_pinned'];
|
|
}
|
|
else
|
|
{
|
|
unset( $data['additional']['pinned'] );
|
|
}
|
|
|
|
if ( isset( $values['node_move_featured'] ) and $values['node_move_featured'] != 'any' )
|
|
{
|
|
$data['additional']['featured'] = $values['node_move_featured'];
|
|
}
|
|
else
|
|
{
|
|
unset( $data['additional']['featured'] );
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Actually perform the mass move operation (called from the wizard helper)
|
|
*
|
|
* @param mixed $data Data from the wizard helper
|
|
* @param Model $node Node we are working with
|
|
* @param string $contentItemClass Content item class (if there is one)
|
|
* @return mixed
|
|
*/
|
|
protected function _performMassMove( mixed $data, Model $node, string $contentItemClass ): mixed
|
|
{
|
|
if ( isset( Request::i()->confirm ) )
|
|
{
|
|
Session::i()->csrfCheck();
|
|
|
|
Task::queue( 'core', 'DeleteOrMoveContent', $data );
|
|
|
|
if ( isset( $data['moveTo'] ) )
|
|
{
|
|
$newNode = $data['moveToClass']::load( $data['moveTo'] );
|
|
Session::i()->log( 'acplog__node_mass_move', array( $this->title => TRUE, $node->titleForLog() => FALSE, $newNode->titleForLog() => FALSE ) );
|
|
Output::i()->redirect( $this->url->setQueryString( array( 'root' => ( $node->parent() ? $node->parent()->_id : '' ) ) ), 'node_mass_content_moving' );
|
|
}
|
|
else
|
|
{
|
|
Session::i()->log( 'acplog__node_mass_delete', array( $this->title => TRUE, $node->titleForLog() => FALSE ) );
|
|
Output::i()->redirect( $this->url->setQueryString( array( 'root' => ( $node->parent() ? $node->parent()->_id : '' ) ) ), 'node_mass_content_deleting' );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
$number = $node->getContentItems( NULL, NULL, $node->massMoveorDeleteWhere( $data ), TRUE );
|
|
|
|
/* @var Item $contentItemClass */
|
|
return Theme::i()->getTemplate( 'global', 'core' )->nodeMoveDeleteContent( $this->url->setQueryString( array( 'do' => 'massManageContent', 'id' => $node->_id, 'subnode' => Request::i()->subnode ) )->csrf(), Member::loggedIn()->language()->addToStack( $contentItemClass::$title . '_pl_lc' ), $number, isset( $data['moveTo'] ) ? $data['moveToClass']::load( $data['moveTo'] ) : NULL );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Search
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function search() : void
|
|
{
|
|
$rows = array();
|
|
|
|
/* Get results */
|
|
$nodeClass = $this->nodeClass;
|
|
$results = $nodeClass::search( '_title', Request::i()->input, '_title' );
|
|
|
|
/* Get results of subnodes */
|
|
if ( isset( $nodeClass::$subnodeClass ) )
|
|
{
|
|
$subnodeClass = $nodeClass::$subnodeClass;
|
|
$results = array_merge( $results, array_values( $subnodeClass::search( '_title', Request::i()->input, '_title' ) ) );
|
|
|
|
usort( $results, function( $a, $b ) {
|
|
return strnatcasecmp( $a->_title, $b->_title );
|
|
} );
|
|
}
|
|
|
|
/* Convert to HTML */
|
|
foreach ( $results as $result )
|
|
{
|
|
$id = ( $result instanceof $this->nodeClass ? '' : 's.' ) . $result->_id;
|
|
$rows[ $id ] = $this->_getRow($result, FALSE, TRUE);
|
|
}
|
|
|
|
Output::i()->output = Theme::i()->getTemplate( 'trees', 'core' )->rows( $rows, '' );
|
|
}
|
|
|
|
/**
|
|
* Allow overloading to change how the title is displayed in the tree
|
|
*
|
|
* @param $node Model Node
|
|
* @return string
|
|
*/
|
|
protected static function nodeTitle( Model $node ): string
|
|
{
|
|
return $node->_title;
|
|
}
|
|
|
|
/**
|
|
* Manage Node Groups
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function nodeGroups() : void
|
|
{
|
|
$nodeClass = $this->nodeClass;
|
|
|
|
$tree = new Tree(
|
|
$this->url->setQueryString( 'do', 'nodeGroups' ),
|
|
NodeGroup::$nodeTitle,
|
|
array( $this, '_getNodeGroupRoots' ),
|
|
array( $this, '_getNodeGroupRow' ),
|
|
null,
|
|
null,
|
|
function(){
|
|
return [
|
|
'add' => [
|
|
'icon' => 'plus',
|
|
'title' => 'new_node_group',
|
|
'link' => $this->url->setQueryString( 'do', 'nodeGroupForm' ),
|
|
'data' => [ 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack( 'new_node_group' ) ]
|
|
]
|
|
];
|
|
}
|
|
);
|
|
|
|
/* Figure out the header blurb */
|
|
try
|
|
{
|
|
$langStringPlural = Member::loggedIn()->language()->get( '__defart_nodegroup__' . $nodeClass->application . '_pl' );
|
|
$langString = Member::loggedIn()->language()->get( '__defart_nodegroup__' . $nodeClass->application );
|
|
}
|
|
catch( \Exception )
|
|
{
|
|
$langStringPlural = Member::loggedIn()->language()->get( '__defart_nodegroup__core_pl' );
|
|
$langString = Member::loggedIn()->language()->get( '__defart_nodegroup__core' );
|
|
}
|
|
|
|
Output::i()->title = Member::loggedIn()->language()->addToStack( NodeGroup::$nodeTitle );
|
|
Output::i()->breadcrumb[] = [ $this->url, $this->title ];
|
|
Output::i()->breadcrumb[] = [ null, Member::loggedIn()->language()->addToStack( NodeGroup::$nodeTitle ) ];
|
|
Output::i()->output = Theme::i()->getTemplate( 'forms','core' )->blurb( Member::loggedIn()->language()->addToStack( 'node_groups_header_desc', null, [ 'sprintf' => [ $langStringPlural, $langString ] ] ) );
|
|
Output::i()->output .= $tree;
|
|
}
|
|
|
|
/**
|
|
* Return all node groups for this node class
|
|
*
|
|
* @return array
|
|
*/
|
|
public function _getNodeGroupRoots() : array
|
|
{
|
|
$roots = [];
|
|
foreach( NodeGroup::roots() as $node )
|
|
{
|
|
if( $node->class == $this->nodeClass )
|
|
{
|
|
$roots[ $node->_id ] = $this->_getNodeGroupRow( $node );
|
|
}
|
|
}
|
|
return $roots;
|
|
}
|
|
|
|
/**
|
|
* Formatted node group row
|
|
*
|
|
* @param mixed $id
|
|
* @return string
|
|
*/
|
|
public function _getNodeGroupRow( mixed $id ) : string
|
|
{
|
|
if ( $id instanceof Model)
|
|
{
|
|
$node = $id;
|
|
$id = $node->_id;
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
$node = NodeGroup::load( $id );
|
|
}
|
|
catch( OutOfRangeException $e )
|
|
{
|
|
Output::i()->error( 'node_error', '2S101/P', 404, '' );
|
|
}
|
|
}
|
|
|
|
$buttons = [
|
|
'edit' => [
|
|
'icon' => 'pencil',
|
|
'title' => 'edit',
|
|
'link' => $this->url->setQueryString( [ 'do' => 'nodeGroupForm', 'id' => $node->_id ] ),
|
|
'data' => [ 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack( 'edit' ) ]
|
|
],
|
|
'delete' => [
|
|
'icon' => 'times-circle',
|
|
'title' => 'delete',
|
|
'link' => $this->url->setQueryString( [ 'do' => 'nodeGroupDelete', 'id' => $node->_id ] )->csrf(),
|
|
'data' => [ 'delete' => '' ]
|
|
]
|
|
];
|
|
|
|
return Theme::i()->getTemplate( 'trees', 'core' )->row(
|
|
$this->url->setQueryString( 'do', 'nodeGroups' ),
|
|
$id,
|
|
static::nodeTitle( $node ),
|
|
false,
|
|
$buttons
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Add/Edit a node group
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function nodeGroupForm() : void
|
|
{
|
|
$group = null;
|
|
if( isset( Request::i()->id ) )
|
|
{
|
|
try
|
|
{
|
|
$group = NodeGroup::load( Request::i()->id );
|
|
if( $group->class != $this->nodeClass )
|
|
{
|
|
throw new OutOfRangeException;
|
|
}
|
|
}
|
|
catch( OutOfRangeException ){}
|
|
}
|
|
|
|
if( $group === null )
|
|
{
|
|
$group = new NodeGroup;
|
|
$group->class = $this->nodeClass;
|
|
}
|
|
|
|
$form = new Form;
|
|
$group->form( $form );
|
|
if( $values = $form->values() )
|
|
{
|
|
$group->saveForm( $group->formatFormValues( $values ) );
|
|
|
|
Output::i()->redirect( $this->url->setQueryString( 'do', 'nodeGroups' ) );
|
|
}
|
|
|
|
/* Appify the language strings */
|
|
try
|
|
{
|
|
$langString = Member::loggedIn()->language()->get( '__defart_nodegroup__' . $this->nodeClass->application );
|
|
}
|
|
catch( \Exception )
|
|
{
|
|
$langString = Member::loggedIn()->language()->get( '__defart_nodegroup__core' );
|
|
}
|
|
|
|
Member::loggedIn()->language()->words['node_group_name_desc'] = Member::loggedIn()->language()->addToStack( 'node_group_name__desc', null, [ 'sprintf' => [ $langString ] ] );
|
|
|
|
Output::i()->output = (string) $form;
|
|
}
|
|
|
|
/**
|
|
* Delete a node group
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function nodeGroupDelete() : void
|
|
{
|
|
Request::i()->confirmedDelete();
|
|
|
|
try
|
|
{
|
|
NodeGroup::load( Request::i()->id )->delete();
|
|
}
|
|
catch( OutOfRangeException ){}
|
|
|
|
Output::i()->redirect( $this->url->setQueryString( 'do', 'nodeGroups' ) );
|
|
}
|
|
} |