pagesFound()) { return false; } if ($this->noPageTitleFound()) { return false; } if ($this->notCorrectFieldNames()) { return false; } if ($this->duplicatedFieldNames()) { return false; } if ($this->duplicatedOptionNames()) { return false; } return true; } public function customMenu() { if ($this->notCorrectMenuPosition()) { return false; } if ($this->notCorrectMenuTitle()) { return false; } if (!$this->menuIcon()) { return false; } return true; } public function existingMenu() { if ($this->notCorrectExistingMenu()) { return false; } return true; } public function existingMenuMenuSlug() { if (is_null($slug = Request::post()['existing_menu']['menu_slug'])) { return ''; } $defaultSlugs = Collection::make(Data::defaultWordpressMenuPageSlugs()); if (!$defaultSlugs->contains($slug)) { return $this->wpDieMessage('Unknown slug!'); } return $slug; } /** * Validate the name and the slug for an existing WordPress menu. * * @return void */ public function notCorrectExistingMenu() { if (is_null($slug = Request::post()['existing_menu']['menu_slug'])) { return; } $defaultSlugs = Collection::make(Data::defaultWordpressMenuPageSlugs()); if (!$defaultSlugs->contains($slug)) { return $this->wpDieMessage('Unknown slug!'); } return false; } /** * Validate if there are pages found. * * @return boolean */ public function pagesFound() { if (!isset(Request::input()['pages'])) { return false; } return true; } public function noPageTitleFound() { if (!isset(Request::input()['pages'])) { return []; } $titles = Data::requestGetSubpages()->pluck('menu_title'); $checkTitles = $titles->filter(function ($title) { if (!$title) { return; } return $title; })->count(); if ($checkTitles !== $titles->count()) { return ($titles->count() - $checkTitles); } return false; } public function notCorrectFieldNames() { if (!isset(Request::input()['pages'])) { return []; } $list = Data::getFieldsWithFieldNames(); $checkList = $list->filter(function ($name) { if (!$name) { return; } preg_match('/[_]*[A-Za-z0-9]\w*/', $name, $matches); if (!$matches) { return $this->wpDieMessage('No valid field name: ' . $name); } if (strlen($name) > 191) { return $this->wpDieMessage('The field name may not be greater than 191 characters: ' . $name); } // Option names are lowercase (changelog 0.9.8); the editor already refuses capitals. if ($name !== strtolower($name)) { return false; } return trim($name) === $matches[0]; }); return $list->diff($checkList)->all(); } /** * The fields that get an option but were posted without a name for it: [pageKey, fieldKey] of each. * Fields that only print something (a title, a rule) need no name. * * @return array */ public function fieldsWithoutName(): array { $registry = FieldRegistry::instance(); $missing = []; foreach (Request::input()['pages'] ?? [] as $pageKey => $page) { foreach ($page['fields'] ?? [] as $fieldKey => $field) { $type = $registry->get($field['type'] ?? ''); if ($type !== null && $type->hasOption() && trim((string) ($field['field_name'] ?? '')) === '') { $missing[] = [$pageKey, $fieldKey]; } } } return $missing; } public function duplicatedFieldNames() { if (!isset(Request::input()['pages'])) { return []; } $coutedFields = Collection::make(array_count_values(Data::getFieldsWithFieldNames()->toArray())); return $coutedFields->filter(function ($field) { return $field > 1; })->all(); } public function duplicatedOptionNames() { if (!isset(Request::input()['pages'])) { return []; } $acceptedFieldNames = Data::getFieldsWithFieldNames() ->diff(Data::getAllOptionNamesExcludeCurrentPageFieldNames()) ->all(); return Data::getFieldsWithFieldNames() ->diff($acceptedFieldNames) ->all(); } public function notCorrectMenuPosition() { if (!isset(Request::input()['menu']['menu_position'])) { return 'no request'; } $menuPosition = Request::input()['menu']['menu_position']; if (!$menuPosition) { return 'empty'; } // 1 to 150, and at most three decimals (150.999): what the editor allows too. preg_match('/^(?:1[0-4][0-9]|150|[1-9][0-9]?)(?:\.[0-9]{1,3})?$/', $menuPosition, $matches); if (!$matches) { return $menuPosition; } if (!$matches[0]) { return $menuPosition; } return false; } public function notCorrectMenuTitle() { if (!isset(Request::input()['menu']['menu_title'])) { return 'no request'; } $menuTitle = Request::input()['menu']['menu_title']; if (!$menuTitle) { return 'empty'; } $sanitizedMenuTitle = wp_strip_all_tags(sanitize_text_field(Request::input()['menu']['menu_title'])); if ($menuTitle !== $sanitizedMenuTitle) { $notValidCharacters = preg_replace("/[(\\\)$sanitizedMenuTitle]/", '', $menuTitle); return htmlspecialchars($notValidCharacters); } return false; } /** * Validate the menu icon. * * @return void|true */ public function menuIcon() { if (!isset(Request::input()['menu']['menu_icon'])) { return 'no request'; } $menuIcon = Request::input()['menu']['menu_icon']; $menuIconName = preg_replace('/dashicons-/', '', $menuIcon); $correctIcon = Dashicons::allIconsCollection()->whereIn([0], $menuIconName)->all(); if (!$correctIcon) { return $this->wpDieMessage(htmlspecialchars($menuIcon) . '? What are you doing?'); } return true; } /** * Validate a class attribute. * * @param string $class * @return string The class attribute as it was given; stops the request for wrong class names. */ public function classAttribute($class = '') { if (empty($class)) { return $class; } // Extra spaces between (or around) the class names do no harm. $classNames = preg_split('/\s+/', $class, -1, PREG_SPLIT_NO_EMPTY); if (!$classNames) { return $class; } if (!$this->valuesAreAlphanumericDashesAll($classNames)) { $this->wpDieMessage('Wrong class names.'); } return $class; } /** * Determine if the given $values are allowed. * Checked for alphanumeric and underscores. * * @param array $values * @return boolean */ public function valuesAreAlphanumericUnderscore(array $values) { $values = Collection::make($values); $checkedValues = $values->filter(function ($value) { preg_match('/[A-Za-z0-9_]+/', $value, $matches); // A value without any allowed character has no match at all. return isset($matches[0]) && trim($value) === $matches[0]; })->all(); return $values->diff($checkedValues)->all() ? false : true; } /** * Determine if the given $values are allowed. * Checked for alphanumeric, dashes and underscores. * * @param array $values * @return boolean */ public function valuesAreAlphanumericDashesAll(array $values) { $values = Collection::make($values); $checkedValues = $values->filter(function ($value) { preg_match('/[A-Za-z0-9_-]+/', $value, $matches); if (!$matches) { return false; } return trim($value) === $matches[0]; })->all(); return $values->diff($checkedValues)->all() ? false : true; } /** * Determine if the given $values are allowed. * Checked for alphanumeric. * * @param array $values * @return boolean */ public function valuesAreAlphanumeric(array $values) { $values = Collection::make($values); $checkedValues = $values->filter(function ($value) { preg_match('/[A-Za-z0-9]+/', $value, $matches); return trim($value) === $matches[0]; })->all(); return $values->diff($checkedValues)->all() ? false : true; } /** * Determine if the given $values are all numbers. * * @param array $values * @param int|null $length * @return boolean */ public function valuesAreNumbers(array $values, $length = null) { $values = Collection::make($values); $checkedValuesIfNumeric = $values->map(function ($value) { return (intval($value) >= 0 && is_numeric($value)) ? $value : null; })->all(); if ($values->diff($checkedValuesIfNumeric)->all()) { return false; } if (is_null($length)) { return true; } if (!absint($length)) { return false; } $checkedValuesForCorrectLength = $values->map(function ($value) use ($length) { if (strlen($value) !== $length) { return null; } return $value; })->all(); return $values->diff($checkedValuesForCorrectLength)->all() ? false : true; } /** * Validate a number. * * @param int $number * @return int */ public function number($number) { if (!is_numeric($number)) { $this->wpDieMessage(); } if (!$this->valuesAreNumbers([$number])) { $this->wpDieMessage(); } return $number; } /** * The position of a page in an existing menu: empty, or a whole number from 0 to 100. * * @param mixed $number * @return int|null */ public function integerOrEmpty($number) { if ($number === '' || $number === null) { return null; } if (!preg_match('/^[0-9]+$/', (string) $number) || (int) $number > 100) { $this->wpDieMessage('Not a number from 0 to 100.'); } return (int) $number; } /** * Determine if the given $values are dupicate * * @param array $values * @return boolean */ public function valuesAreDuplicated(array $values) { return count($values) !== count(array_unique($values)); } /** * Validate fieldId. * * @param string $id * @return int */ public function fieldId($id) { $id = preg_replace('/[^0-9]/', '', $id); if (strlen($id) !== 13) { $this->wpDieMessage(); } return (int) $id; } /** * Validate tableList. * * @param array $list * @return array */ public function tableList(array $list) { $list = collection::make($list); $values = $list->pluck('value')->all(); $ids = $list->pluck('id')->all(); if ($this->valuesAreDuplicated($values)) { $this->wpDieMessage(); } if (!$this->valuesAreAlphanumericUnderscore($values)) { $this->wpDieMessage(); } if (!$this->valuesAreNumbers($ids, 13)) { $this->wpDieMessage(); } return $list->map(function ($item) { $item['label'] = sanitize_text_field($item['label']); return $item; })->all(); } /** * Validate text field. * * @param string $value * @return string */ public function textField($value) { return sanitize_text_field($value); } /** * Validate an e-mail address. An invalid value becomes empty, like sanitize_email() does. * * @param string $value * @return string */ public function email($value) { return sanitize_email($value); } /** * Validate a URL. A disallowed scheme or malformed value becomes empty, like esc_url_raw() does. * * @param string $value * @return string */ public function url($value) { return esc_url_raw($value); } /** * Validate a date, the value of (Y-m-d). A malformed or non-existent date (2024-02-30) * becomes empty, like sanitize_email() and esc_url_raw() do for their own fields. * * @param mixed $value * @return string */ public function date($value) { return $this->exactDateFormat($value, 'Y-m-d'); } /** * Validate a time, the value of without seconds (H:i). An invalid value becomes empty. * * @param mixed $value * @return string */ public function time($value) { return $this->exactDateFormat($value, 'H:i'); } /** * Validate a date and time together, the value of without seconds * (Y-m-d\TH:i). An invalid value becomes empty. * * @param mixed $value * @return string */ public function dateTime($value) { return $this->exactDateFormat($value, 'Y-m-d\TH:i'); } /** * A value that DateTime::createFromFormat() parses back to exactly the same string in the given format; * anything else (an invalid date, a different separator, trailing text) becomes empty. * * @param mixed $value * @param string $format * @return string */ private function exactDateFormat($value, string $format): string { if (!is_string($value) || $value === '') { return ''; } $date = \DateTime::createFromFormat($format, $value); if (!$date || $date->format($format) !== $value) { return ''; } return $value; } /** * Validate string if alphanumeric, dashes and underscores. * * @param string $value * @return string */ public function alphanumericDashesAll($value) { if (!$this->valuesAreAlphanumericDashesAll([$value])) { $this->wpDieMessage(); } return $value; } /** * Validate string if alphanumeric. * * @param string $value * @return string */ public function alphanumeric($value) { if (!$this->valuesAreAlphanumeric([$value])) { $this->wpDieMessage(); } return $value; } /** * Validate field types. * * @param object $types * @param array $correctTypes * @return void */ public function fieldTypes($types, array $correctTypes) { $hasWrongTypes = $types->whereNotIn('type', $correctTypes)->all(); if ($hasWrongTypes) { $this->wpDieMessage(); } } /** * Validate toggle if true or false. * * @param string $toggle * @return void|string */ public function toggle($toggle) { if (!($toggle === 'true' || $toggle === 'false')) { $this->wpDieMessage(); } return $toggle; } /** * Validate toggle if true or false. * * @param string $toggle * @return boolean */ public function toggleBoolean($toggle) { if (!($toggle === 'true' || $toggle === 'false')) { $this->wpDieMessage(); } return $toggle === 'true'; } public function textFieldStyle($class) { $classList = Collection::make([ 'regular_text', 'all_options', 'small_text', 'large_text' ]); if (!$classList->contains($class)) { $this->wpDieMessage(); } return $class; } public function contains($valueToContains, $fieldItem) { $classList = Collection::make($valueToContains); if (!$classList->contains($fieldItem)) { $this->wpDieMessage(); } return $fieldItem; } public function textareaStyle($class) { $classList = Collection::make([ 'standard', 'all_options', 'large_text' ]); if (!$classList->contains($class)) { $this->wpDieMessage(); } return $class; } public function toggleTextFormat($field) { if (!isset($field['toggle_text_format'])) { return; } if ($field['toggle_text_format'] !== '1') { $this->wpDieMessage(); } return $field['toggle_text_format']; } public function toggleAutoload($field) { if (!isset($field['toggle_autoload'])) { return 'yes'; } if ($field['toggle_autoload'] !== 'no') { $this->wpDieMessage(); } return $field['toggle_autoload']; } /** * The "Use in shortcode and blocks" switch. The form posts "yes" when it is on and nothing when it is off; an * import of a stored field posts "no". Anything else is not something the editor sends. * * @param mixed $value */ public function toggleFrontend($value): string { if ($value === null || $value === 'no') { return 'no'; } if ($value !== 'yes') { $this->wpDieMessage(); } return 'yes'; } public function toggleOnOff($fieldItem) { if (!isset($fieldItem)) { return; } if ($fieldItem !== 'on' && $fieldItem !== 'off') { $this->wpDieMessage(); } return $fieldItem; } public function imageExtensions($extensions) { if ($extensions === []) { return ['types' => [ 'jpeg' => 'on', 'png' => 'on', 'gif' => 'on', 'svg' => 'on', 'ico' => 'on' ]]; } $list = [ 'jpeg', 'png', 'gif', 'svg', 'ico' ]; Collection::make($extensions['types'])->map(function ($state, $type) use ($list) { if (!in_array($type, $list)) { $this->wpDieMessage(); } }); return $extensions; } /** * Validate the allowed extensions of a file field, the same shape as imageExtensions() but for the document * types a file field offers. * * @param array $extensions * @return array */ public function fileExtensions($extensions) { if ($extensions === []) { return ['types' => [ 'pdf' => 'on', 'doc' => 'on', 'docx' => 'on', 'xls' => 'on', 'xlsx' => 'on', 'csv' => 'on', 'zip' => 'on', 'txt' => 'on' ]]; } $list = [ 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'csv', 'zip', 'txt' ]; Collection::make($extensions['types'])->map(function ($state, $type) use ($list) { if (!in_array($type, $list)) { $this->wpDieMessage(); } }); return $extensions; } /** * Validate a text area. * * @param string $text * @return string */ public function textArea($text) { return wp_kses_post($text); } /** * Validate if value is correct hexadecimal color. * * @param string $value * @return boolean */ public function isColorHex($value) { if (preg_match('/^#[a-f0-9]{6}$/i', $value) || preg_match('/^#[a-f0-9]{3}$/i', $value)) { return true; } return false; } /** * Validate a hexadecimal color input. * * @param string $value * @return boolean */ public function colorHex($value) { if (!$value) { return '#ffffff'; } if (!preg_match('/^#[a-f0-9]{6}$/i', $value)) { $this->wpDieMessage(); } return $value; } public function capabilityPage($value) { if ($value === 'editor') { return 'edit_others_posts'; } return 'manage_options'; } public function capabilityMenu() { if (Collection::make(Request::post()['pages'])->contains('role', 'editor')) { return 'edit_others_posts'; } return 'manage_options'; } /** * Value already validated. * * @param mixed $value * @param string $method Gives the name of the method where the validation has been done. * @return mixed */ public function already($value, $method = '') { return $value; } /** * WP die message. * * @param string $message * @return void */ public function wpDieMessage($message = '') { Request::fail($message ?: 'Are you trying to break something?'); } }