Completed
Push — master ( bbb282...43d0b8 )
by Daniel
25s
created

CMSBatchActionHandler::actionByName()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Admin;
4
5
use SilverStripe\ORM\ArrayList;
6
use SilverStripe\ORM\DB;
7
use SilverStripe\ORM\SS_List;
8
use SilverStripe\ORM\Versioning\Versioned;
9
use SilverStripe\ORM\DataObject;
10
use SilverStripe\Security\SecurityToken;
11
use RequestHandler;
12
use Config;
13
use Controller;
14
use SS_HTTPRequest;
15
use SS_HTTPResponse;
16
use InvalidArgumentException;
17
use ArrayData;
18
use Translatable;
19
20
/**
21
 * Special request handler for admin/batchaction
22
 *
23
 * @package framework
24
 * @subpackage admin
25
 */
26
class CMSBatchActionHandler extends RequestHandler {
27
28
	/** @config */
29
	private static $batch_actions = array();
30
31
	private static $url_handlers = array(
32
		'$BatchAction/applicablepages' => 'handleApplicablePages',
33
		'$BatchAction/confirmation' => 'handleConfirmation',
34
		'$BatchAction' => 'handleBatchAction',
35
	);
36
37
	private static $allowed_actions = array(
38
		'handleBatchAction',
39
		'handleApplicablePages',
40
		'handleConfirmation',
41
	);
42
43
	/**
44
	 * @var Controller
45
	 */
46
	protected $parentController;
47
48
	/**
49
	 * @var String
50
	 */
51
	protected $urlSegment;
52
53
	/**
54
	 * @var String $recordClass The classname that should be affected
55
	 * by any batch changes. Needs to be set in the actual {@link CMSBatchAction}
56
	 * implementations as well.
57
	 */
58
	protected $recordClass = 'SilverStripe\\CMS\\Model\\SiteTree';
59
60
	/**
61
	 * @param Controller $parentController
62
	 * @param string $urlSegment
63
	 * @param string $recordClass
64
	 */
65
	public function __construct($parentController, $urlSegment, $recordClass = null) {
66
		$this->parentController = $parentController;
67
		$this->urlSegment = $urlSegment;
68
		if($recordClass) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $recordClass of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
69
			$this->recordClass = $recordClass;
70
		}
71
72
		parent::__construct();
73
	}
74
75
	/**
76
	 * Register a new batch action.  Each batch action needs to be represented by a subclass
77
	 * of {@link CMSBatchAction}.
78
	 *
79
	 * @param string $urlSegment The URL Segment of the batch action - the URL used to process this
80
	 * action will be admin/batchactions/(urlSegment)
81
	 * @param string $batchActionClass The name of the CMSBatchAction subclass to register
82
	 * @param string $recordClass
83
	 */
84
	public static function register($urlSegment, $batchActionClass, $recordClass = 'SilverStripe\\CMS\\Model\\SiteTree') {
85
		if(is_subclass_of($batchActionClass, 'SilverStripe\\Admin\\CMSBatchAction')) {
86
			Config::inst()->update(
87
				'SilverStripe\\Admin\\CMSBatchActionHandler',
88
				'batch_actions',
89
				array(
90
					$urlSegment => array(
91
						'class' => $batchActionClass,
92
						'recordClass' => $recordClass
93
					)
94
				)
95
			);
96
		} else {
97
			user_error("CMSBatchActionHandler::register() - Bad class '$batchActionClass'", E_USER_ERROR);
98
		}
99
	}
100
101
	public function Link() {
102
		return Controller::join_links($this->parentController->Link(), $this->urlSegment);
103
	}
104
105
	/**
106
	 * Invoke a batch action
107
	 *
108
	 * @param SS_HTTPRequest $request
109
	 * @return SS_HTTPResponse
110
	 */
111
	public function handleBatchAction($request) {
112
		// This method can't be called without ajax.
113
		if(!$request->isAjax()) {
114
			return $this->parentController->redirectBack();
115
		}
116
117
		// Protect against CSRF on destructive action
118
		if(!SecurityToken::inst()->checkRequest($request)) {
119
			return $this->httpError(400);
120
		}
121
122
		// Find the action handler
123
		$action = $request->param('BatchAction');
124
		$actionHandler = $this->actionByName($action);
125
126
		// Sanitise ID list and query the database for apges
127
		$csvIDs = $request->requestVar('csvIDs');
128
		$ids = $this->cleanIDs($csvIDs);
129
130
		// Filter ids by those which are applicable to this action
131
		// Enforces front end filter in LeftAndMain.BatchActions.js:refreshSelected
132
		$ids = $actionHandler->applicablePages($ids);
133
134
		// Query ids and pass to action to process
135
		$pages = $this->getPages($ids);
136
		return $actionHandler->run($pages);
137
	}
138
139
	/**
140
	 * Respond with the list of applicable pages for a given filter
141
	 *
142
	 * @param SS_HTTPRequest $request
143
	 * @return SS_HTTPResponse
144
	 */
145
	public function handleApplicablePages($request) {
146
		// Find the action handler
147
		$action = $request->param('BatchAction');
148
		$actionHandler = $this->actionByName($action);
149
150
		// Sanitise ID list and query the database for apges
151
		$csvIDs = $request->requestVar('csvIDs');
152
		$ids = $this->cleanIDs($csvIDs);
153
154
		// Filter by applicable pages
155
		if($ids) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $ids of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
156
			$applicableIDs = $actionHandler->applicablePages($ids);
157
		} else {
158
			$applicableIDs = array();
159
		}
160
161
		$response = new SS_HTTPResponse(json_encode($applicableIDs));
162
		$response->addHeader("Content-type", "application/json");
163
		return $response;
164
	}
165
166
	/**
167
	 * Check if this action has a confirmation step
168
	 *
169
	 * @param SS_HTTPRequest $request
170
	 * @return SS_HTTPResponse
171
	 */
172
	public function handleConfirmation($request) {
173
		// Find the action handler
174
		$action = $request->param('BatchAction');
175
		$actionHandler = $this->actionByName($action);
176
177
		// Sanitise ID list and query the database for apges
178
		$csvIDs = $request->requestVar('csvIDs');
179
		$ids = $this->cleanIDs($csvIDs);
180
181
		// Check dialog
182
		if($actionHandler->hasMethod('confirmationDialog')) {
183
			$response = new SS_HTTPResponse(json_encode($actionHandler->confirmationDialog($ids)));
184
		} else {
185
			$response = new SS_HTTPResponse(json_encode(array('alert' => false)));
186
		}
187
188
		$response->addHeader("Content-type", "application/json");
189
		return $response;
190
	}
191
192
	/**
193
	 * Get an action for a given name
194
	 *
195
	 * @param string $name Name of the action
196
	 * @return CMSBatchAction An instance of the given batch action
197
	 * @throws InvalidArgumentException if invalid action name is passed.
198
	 */
199
	protected function actionByName($name) {
200
		// Find the action handler
201
		$actions = $this->batchActions();
202
		if(!isset($actions[$name]['class'])) {
203
			throw new InvalidArgumentException("Invalid batch action: {$name}");
204
		}
205
		return $this->buildAction($actions[$name]['class']);
206
	}
207
208
	/**
209
	 * Return a SS_List of ArrayData objects containing the following pieces of info
210
	 * about each batch action:
211
	 *  - Link
212
	 *  - Title
213
	 *
214
	 * @return ArrayList
215
	 */
216
	public function batchActionList() {
217
		$actions = $this->batchActions();
218
		$actionList = new ArrayList();
219
220
		foreach($actions as $urlSegment => $action) {
221
			$actionObj = $this->buildAction($action['class']);
222
			if($actionObj->canView()) {
223
				$actionDef = new ArrayData(array(
224
					"Link" => Controller::join_links($this->Link(), $urlSegment),
225
					"Title" => $actionObj->getActionTitle(),
226
				));
227
				$actionList->push($actionDef);
228
			}
229
		}
230
231
		return $actionList;
232
	}
233
234
	/**
235
	 * Safely generate batch action object for a given classname
236
	 *
237
	 * @param string $class Class name to check
238
	 * @return CMSBatchAction An instance of the given batch action
239
	 * @throws InvalidArgumentException if invalid action class is passed.
240
	 */
241
	protected function buildAction($class) {
242
		if(!is_subclass_of($class, 'SilverStripe\\Admin\\CMSBatchAction')) {
243
			throw new InvalidArgumentException("{$class} is not a valid subclass of CMSBatchAction");
244
		}
245
		return $class::singleton();
246
	}
247
248
	/**
249
	 * Sanitise ID list from string input
250
	 *
251
	 * @param string $csvIDs
252
	 * @return array List of IDs as ints
253
	 */
254
	protected function cleanIDs($csvIDs) {
255
		$ids = preg_split('/ *, */', trim($csvIDs));
256
		foreach($ids as $k => $id) {
257
			$ids[$k] = (int)$id;
258
		}
259
		return array_filter($ids);
260
	}
261
262
	/**
263
	 * Get all registered actions through the static defaults set by {@link register()}.
264
	 * Filters for the currently set {@link recordClass}.
265
	 *
266
	 * @return array See {@link register()} for the returned format.
267
	 */
268
	public function batchActions() {
269
		$actions = $this->config()->batch_actions;
0 ignored issues
show
Documentation introduced by
The property batch_actions does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
270
		$recordClass = $this->recordClass;
271
		$actions = array_filter($actions, function($action) use ($recordClass) {
272
			return $action['recordClass'] === $recordClass;
273
		});
274
		return $actions;
275
	}
276
277
	/**
278
	 * Safely query and return all pages queried
279
	 *
280
	 * @param array $ids
281
	 * @return SS_List
282
	 */
283
	protected function getPages($ids) {
284
		// Check empty set
285
		if(empty($ids)) {
286
			return new ArrayList();
287
		}
288
289
		$recordClass = $this->recordClass;
290
291
		// Bypass translatable filter
292
		if(class_exists('Translatable') && $recordClass::has_extension('Translatable')) {
293
			Translatable::disable_locale_filter();
294
		}
295
296
		// Bypass versioned filter
297
		if($recordClass::has_extension('SilverStripe\\ORM\\Versioning\\Versioned')) {
298
			// Workaround for get_including_deleted not supporting byIDs filter very well
299
			// Ensure we select both stage / live records
300
			$pages = Versioned::get_including_deleted($recordClass, array(
301
				'"RecordID" IN ('.DB::placeholders($ids).')' => $ids
302
			));
303
		} else {
304
			$pages = DataObject::get($recordClass)->byIDs($ids);
305
		}
306
307
		if(class_exists('Translatable') && $recordClass::has_extension('Translatable')) {
308
			Translatable::enable_locale_filter();
309
		}
310
311
		return $pages;
312
	}
313
314
}
315