Controller
6 days ago
Helper
3 weeks ago
Model
6 days ago
external
6 days ago
view
6 days ago
.gitignore
5 years ago
BuildAutoLoader.php
3 weeks ago
Controller.php
3 weeks ago
Model.php
3 weeks ago
ViewController.php
3 weeks ago
plugin.json
1 month ago
ViewController.php
358 lines
| 1 | <?php |
| 2 | namespace ShortPixel; |
| 3 | |
| 4 | if ( ! defined( 'ABSPATH' ) ) { |
| 5 | exit; // Exit if accessed directly. |
| 6 | } |
| 7 | |
| 8 | use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log; |
| 9 | use ShortPixel\Model\AccessModel as AccessModel; |
| 10 | |
| 11 | /** |
| 12 | * Base controller for view-related functionality. |
| 13 | * |
| 14 | * Extends Controller to add view rendering, POST data handling, nonce verification, |
| 15 | * and HTML output helpers. Most admin page controllers extend this class. |
| 16 | * |
| 17 | * @package ShortPixel |
| 18 | */ |
| 19 | class ViewController extends Controller |
| 20 | { |
| 21 | // protected static $controllers = array(); |
| 22 | |
| 23 | /** |
| 24 | * Tracks which view templates have already been included to prevent double-loading. |
| 25 | * |
| 26 | * @var string[] |
| 27 | */ |
| 28 | protected static $viewsLoaded = array(); |
| 29 | |
| 30 | /** |
| 31 | * Singleton instance of the controller. |
| 32 | * |
| 33 | * @var static|null |
| 34 | */ |
| 35 | protected static $instance; |
| 36 | |
| 37 | /** @var mixed|null Connected model instance. */ |
| 38 | protected $model; // connected model to load. |
| 39 | |
| 40 | /** @var string|null Template file name (without .php extension) to include when loading the view. */ |
| 41 | protected $template = null; // template name to include when loading. |
| 42 | |
| 43 | /** @var array<string, mixed> Data array for passing database data and other values to the view. */ |
| 44 | protected $data = []; // data array for usage with databases data and such |
| 45 | |
| 46 | /** @var array<string, mixed> Sanitized data received from form POST submissions. */ |
| 47 | protected $postData = []; // data coming from form posts. |
| 48 | |
| 49 | /** |
| 50 | * Optional map of POST field names to model field names. |
| 51 | * Keys are the incoming POST names; values are the corresponding model field names. |
| 52 | * |
| 53 | * @var array<string, string>|null |
| 54 | */ |
| 55 | protected $mapper; // Mapper is array of View Name => Model Name. Convert between the two |
| 56 | |
| 57 | /** @var bool Whether a form was submitted in the current request. */ |
| 58 | protected $is_form_submit = false; // Was the form submitted? |
| 59 | |
| 60 | /** @var \stdClass View data object passed into included template files. */ |
| 61 | protected $view; // object to use in the view. |
| 62 | |
| 63 | /** @var string|null URL of the admin page this controller manages, used for redirects. */ |
| 64 | protected $url; // if controller is home to a page, sets the URL here. For redirects and what not. |
| 65 | |
| 66 | /** @var string Nonce action name used when verifying form submissions. */ |
| 67 | protected $form_action = 'sp-action'; |
| 68 | |
| 69 | public static function init() |
| 70 | { |
| 71 | |
| 72 | } |
| 73 | |
| 74 | public function __construct() |
| 75 | { |
| 76 | parent::__construct(); |
| 77 | $this->view = new \stdClass; |
| 78 | // Basic View Construct |
| 79 | $this->view->notices = null; // Notices of class notice, for everything noticable |
| 80 | $this->view->data = null; // Data(base), to separate from regular view data |
| 81 | |
| 82 | } |
| 83 | |
| 84 | /** |
| 85 | * Returns the singleton instance of the calling class, creating it if necessary. |
| 86 | * |
| 87 | * Uses late static binding so subclasses each maintain their own instance. |
| 88 | * |
| 89 | * @return static The singleton instance. |
| 90 | */ |
| 91 | public static function getInstance() { |
| 92 | if (is_null(static::$instance)) { |
| 93 | static::$instance = new static(); |
| 94 | } |
| 95 | |
| 96 | return static::$instance; |
| 97 | } |
| 98 | |
| 99 | /** |
| 100 | * Verifies an incoming form POST against the controller's nonce. |
| 101 | * |
| 102 | * When a valid POST is detected, optionally calls processPostData() to sanitize |
| 103 | * and store the submitted fields. Returns false and terminates execution on a |
| 104 | * hard nonce failure; returns true silently when no POST data is present. |
| 105 | * |
| 106 | * @param bool $processPostData Whether to call processPostData() on valid submission. Default true. |
| 107 | * @return bool True when no POST or POST is valid; false on nonce mismatch with ajaxSave present. |
| 108 | */ |
| 109 | protected function checkPost($processPostData = true) |
| 110 | { |
| 111 | |
| 112 | if(count($_POST) === 0) // no post, nothing to check, return silent. |
| 113 | { |
| 114 | return true; |
| 115 | } |
| 116 | elseif (! isset($_POST['sp-nonce']) || ! wp_verify_nonce( sanitize_key($_POST['sp-nonce']), $this->form_action)) |
| 117 | { |
| 118 | // Obscure issue. Detected other plugin that adds information to $_POST without an actual form submit, which would trigger the nonce check on the settings page. In case this happens, be lenient. |
| 119 | if ( ! isset($_POST['ajaxSave']) || ! isset($_POST['action']) ) |
| 120 | { |
| 121 | return false; |
| 122 | } |
| 123 | Log::addInfo('Check Post fails nonce check, action : ' . $this->form_action, array($_POST) ); |
| 124 | wp_die('Nonce Failed'); |
| 125 | return true; |
| 126 | } |
| 127 | elseif (isset($_POST) && count($_POST) > 0) |
| 128 | { |
| 129 | check_admin_referer( $this->form_action, 'sp-nonce' ); // extra check, when we are wrong here, it dies. |
| 130 | |
| 131 | $this->is_form_submit = true; |
| 132 | if (true === $processPostData) // only processData on form save. |
| 133 | { |
| 134 | $this->processPostData($_POST); |
| 135 | } |
| 136 | |
| 137 | |
| 138 | } |
| 139 | return true; |
| 140 | } |
| 141 | |
| 142 | /** |
| 143 | * Returns the singleton AccessModel instance for permission checks. |
| 144 | * |
| 145 | * @return AccessModel |
| 146 | */ |
| 147 | public function access() |
| 148 | { |
| 149 | return AccessModel::getInstance(); |
| 150 | } |
| 151 | |
| 152 | /** |
| 153 | * Loads and includes a view template file from the class/view/ directory. |
| 154 | * |
| 155 | * If $unique is true (default), each template is included only once per request. |
| 156 | * Passes $this->view and $this as local variables ($view and $controller) to the template. |
| 157 | * |
| 158 | * @param string|null $template Relative template name (without .php). Falls back to $this->template. |
| 159 | * @param bool $unique Whether to prevent loading the same template more than once. Default true. |
| 160 | * @param array $args Additional arguments made available as $view->template_args inside the template. |
| 161 | * @return bool|void False when no valid template name is available; void otherwise. |
| 162 | */ |
| 163 | public function loadView($template = null, $unique = true, $args = []) |
| 164 | { |
| 165 | // load either param or class template. |
| 166 | $template = (is_null($template)) ? $this->template : $template; |
| 167 | |
| 168 | if (is_null($template) ) |
| 169 | { |
| 170 | return false; |
| 171 | } |
| 172 | elseif (strlen(trim($template)) == 0) |
| 173 | { |
| 174 | return false; |
| 175 | } |
| 176 | |
| 177 | $view = $this->view; |
| 178 | $view->template_args = $args; // local pass only for this view, useful for snippets, not main controllers. |
| 179 | $controller = $this; |
| 180 | |
| 181 | $template_path = \wpSPIO()->plugin_path('class/view/' . $template . '.php'); |
| 182 | if (file_exists($template_path) === false) |
| 183 | { |
| 184 | Log::addError("View $template could not be found in " . $template_path, |
| 185 | array('class' => get_class($this))); |
| 186 | } |
| 187 | elseif ($unique === false || ! in_array($template, self::$viewsLoaded)) |
| 188 | { |
| 189 | include($template_path); |
| 190 | self::$viewsLoaded[] = $template; |
| 191 | } |
| 192 | else { |
| 193 | |
| 194 | } |
| 195 | |
| 196 | } |
| 197 | |
| 198 | /** Manually add data to this viewcontroller |
| 199 | * |
| 200 | * @param array $data Data to add. |
| 201 | * @return void |
| 202 | */ |
| 203 | public function addData($data) |
| 204 | { |
| 205 | $this->data = array_merge($this->data, $data); |
| 206 | } |
| 207 | |
| 208 | /** Loads a view and then returns it as html string. Handy for passing back snippets in JSON and other things. |
| 209 | * |
| 210 | * @param string $template Name of template |
| 211 | * @return string HTML string of view loaded. |
| 212 | */ |
| 213 | public function returnView($template = null) |
| 214 | { |
| 215 | $bool = ob_start(); |
| 216 | $html = ''; |
| 217 | |
| 218 | if (true === $bool) |
| 219 | { |
| 220 | $this->loadView($template, false); |
| 221 | $html = ob_get_contents(); |
| 222 | ob_end_clean(); |
| 223 | } |
| 224 | else |
| 225 | { |
| 226 | Log::addError('Output buffer failed requesting returnView!' . $template); |
| 227 | } |
| 228 | |
| 229 | return $html; |
| 230 | } |
| 231 | |
| 232 | /** |
| 233 | * Outputs an inline help icon linking to external documentation. |
| 234 | * |
| 235 | * @param string $url The documentation URL to link to (will be escaped). |
| 236 | * @return void |
| 237 | */ |
| 238 | protected function printInlineHelp($url) |
| 239 | { |
| 240 | |
| 241 | $output = '<i class="documentation dashicons dashicons-editor-help" data-link="' . esc_url($url). '"></i>'; |
| 242 | echo $output; |
| 243 | |
| 244 | } |
| 245 | |
| 246 | /** |
| 247 | * Renders and echoes a toggle switch button element. |
| 248 | * |
| 249 | * Accepts an array of arguments that control the name, checked state, label, |
| 250 | * CSS classes, data attributes, and disabled state of the rendered switch. |
| 251 | * |
| 252 | * @param array $args { |
| 253 | * Optional. Overrides for the switch button defaults. |
| 254 | * |
| 255 | * @type string $name Input name attribute. Default ''. |
| 256 | * @type bool $checked Whether the switch is checked. Default false. |
| 257 | * @type string $label Label text displayed beside the switch. Default ''. |
| 258 | * @type string|false $switch_class CSS class for the outer <switch> element. Default false. |
| 259 | * @type string $input_class CSS class for the <input> element. Default 'switch'. |
| 260 | * @type array $data Additional data attributes as pre-formatted strings. Default []. |
| 261 | * @type bool $disabled Whether the switch is disabled. Default false. |
| 262 | * } |
| 263 | * @return void |
| 264 | */ |
| 265 | protected function printSwitchButton($args) |
| 266 | { |
| 267 | $defaults = array( |
| 268 | 'name' => '', |
| 269 | 'checked' => false, |
| 270 | 'label' => '', |
| 271 | 'switch_class' => false, |
| 272 | 'input_class' => 'switch', |
| 273 | 'data' => [], |
| 274 | 'disabled' => false, |
| 275 | 'tooltip_link' => '', |
| 276 | ); |
| 277 | |
| 278 | $args = wp_parse_args($args, $defaults); |
| 279 | |
| 280 | $switchclass = ($args['switch_class'] !== false) ? 'class="' . $args['switch_class'] . '"' : ''; |
| 281 | $inputclass = $args['input_class']; |
| 282 | $checked = checked($args['checked'], true, false); |
| 283 | $name = esc_attr($args['name']); |
| 284 | $label = esc_attr($args['label']); |
| 285 | |
| 286 | $tooltip = ''; |
| 287 | if (! empty($args['tooltip_link'])) { |
| 288 | $tooltip = sprintf('<i class="documentation dashicons dashicons-editor-help" data-link="%s"></i>', esc_attr($args['tooltip_link'])); |
| 289 | } |
| 290 | |
| 291 | $data = implode(' ', $args['data']); |
| 292 | |
| 293 | $disabled = $args['disabled']; |
| 294 | $disabled = (true === $disabled) ? 'disabled' : ''; |
| 295 | |
| 296 | $output = sprintf('<switch %s> |
| 297 | <label> |
| 298 | <input type="checkbox" class="%s" name="%s" value="1" %s %s %s> |
| 299 | <div class="the_switch"> </div> |
| 300 | %s%s |
| 301 | </label> |
| 302 | </switch>', $switchclass, $inputclass, $name, $checked, $disabled, $data, $label, $tooltip); |
| 303 | |
| 304 | echo $output; |
| 305 | } |
| 306 | |
| 307 | /** Accepts POST data, maps, checks missing fields, and applies sanitization to it. |
| 308 | * |
| 309 | * Applies any field name mappings defined in $this->mapper, then either sanitizes |
| 310 | * each field via the connected model or falls back to sanitize_text_field() when no |
| 311 | * model is set. The result is stored in $this->postData. |
| 312 | * |
| 313 | * @param array $post Raw POST data (typically $_POST). |
| 314 | * @return array<string, mixed>|bool Sanitized post data array, or true if no model is set. |
| 315 | */ |
| 316 | protected function processPostData($post) |
| 317 | { |
| 318 | |
| 319 | // If there is something to map, map. |
| 320 | if ($this->mapper && is_array($this->mapper) && count($this->mapper) > 0) |
| 321 | { |
| 322 | foreach($this->mapper as $item => $replace) |
| 323 | { |
| 324 | if ( isset($post[$item])) |
| 325 | { |
| 326 | $post[$replace] = $post[$item]; |
| 327 | unset($post[$item]); |
| 328 | } |
| 329 | } |
| 330 | } |
| 331 | |
| 332 | if (is_null($this->model) && is_null($model)) |
| 333 | { |
| 334 | foreach($post as $name => $value ) |
| 335 | { |
| 336 | $this->postData[sanitize_text_field($name)] = sanitize_text_field($value); |
| 337 | return true; |
| 338 | } |
| 339 | } |
| 340 | else |
| 341 | { |
| 342 | $this->postData = $this->model->getSanitizedData($post, false); |
| 343 | } |
| 344 | |
| 345 | return $this->postData; |
| 346 | |
| 347 | } |
| 348 | |
| 349 | /** Sets the URL of the admin page */ |
| 350 | public function setControllerURL($url) |
| 351 | { |
| 352 | $this->url = $url; |
| 353 | } |
| 354 | |
| 355 | |
| 356 | |
| 357 | } // controller |
| 358 |