├── .codeclimate.yml ├── img └── loading.gif ├── src ├── Parser │ ├── InputArgParser.php │ ├── AuthorArgParser.php │ └── TaxonomyArgParser.php ├── Enum │ ├── RequestMethod.php │ ├── Relation.php │ ├── Operator.php │ ├── Compare.php │ ├── InputFormat.php │ ├── FieldType.php │ ├── DataType.php │ ├── BasicEnum.php │ └── RequestVar.php ├── HttpRequest.php ├── FieldGroup.php ├── Helper │ ├── Pagination.php │ └── ResultsRange.php ├── StdObject.php ├── AjaxConfig.php ├── DateQuery.php ├── TaxQuery.php ├── DateInputBuilder.php ├── TermsWalker.php ├── Form.php ├── Type.php ├── Field.php ├── Input.php ├── Query.php ├── MetaQuery.php ├── Validator.php ├── Factory.php ├── InputBuilder.php └── InputMarkup.php ├── .gitignore ├── phpunit.xml ├── .travis.yml ├── tests ├── bootstrap.php ├── TestHttpRequest.php ├── TestMetaQuery.php ├── TestField.php ├── TestForm.php ├── TestType.php ├── TestInput.php ├── TestValidator.php ├── TestInputBuilder.php └── TestFactory.php ├── README.md ├── templates └── template-ajax-results.php ├── config └── form.default.php ├── init.php ├── lib.php ├── bin └── install-wp-tests.sh ├── wpas.php └── js └── scripts.js /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | languages: 2 | PHP: true 3 | exclude_paths: 4 | - "tests/*" -------------------------------------------------------------------------------- /img/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raideus/wp-advanced-search/HEAD/img/loading.gif -------------------------------------------------------------------------------- /src/Parser/InputArgParser.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | ./tests/ 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.3 5 | - 5.4 6 | - 5.5 7 | 8 | env: 9 | - WP_VERSION=latest WP_MULTISITE=0 10 | - WP_VERSION=latest WP_MULTISITE=1 11 | - WP_VERSION=4.1 WP_MULTISITE=0 12 | - WP_VERSION=4.1 WP_MULTISITE=1 13 | 14 | before_script: 15 | - bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION 16 | 17 | script: phpunit 18 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | '; 8 | const greq = '>='; 9 | const less = '<'; 10 | const leq ='<='; 11 | const like = 'LIKE'; 12 | const notlike = 'NOT LIKE'; 13 | const in = 'IN'; 14 | const notin = 'NOT IN'; 15 | const between = 'BETWEEN'; 16 | const notbetween = 'NOT BETWEEN'; 17 | const exists = 'EXISTS'; 18 | const notexists = 'NOT EXISTS'; 19 | const _default = self::equal; 20 | } -------------------------------------------------------------------------------- /src/Enum/InputFormat.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 |

14 |

Author:    Date:

15 | 16 |

Read more...

17 | 18 | 19 | 20 | 21 | 22 |

Sorry, no posts matched your criteria.

23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/HttpRequest.php: -------------------------------------------------------------------------------- 1 | request = $request; 17 | } 18 | 19 | public function get($key, $default = null) { 20 | return self::getVal($this->request, $key, $default); 21 | } 22 | 23 | public function all() { 24 | return $this->request; 25 | } 26 | 27 | } 28 | 29 | -------------------------------------------------------------------------------- /config/form.default.php: -------------------------------------------------------------------------------- 1 | array('post'), 'posts_per_page' => 10); 7 | 8 | $args['config']['meta_key_relation'] = 'AND'; 9 | $args['config']['taxonomy_relation'] = 'AND'; 10 | 11 | $args['fields'][] = array( 12 | 'type' => 'search', 13 | 'label' => 'Search' 14 | ); 15 | 16 | $args['fields'][] = array( 17 | 'type' => 'taxonomy', 18 | 'label' => 'Category', 19 | 'taxonomy' => 'category', 20 | 'format' => 'select', 21 | 'allow_null' => true, 22 | 'operator' => 'AND' 23 | ); 24 | 25 | $args['fields'][] = array( 26 | 'type' => 'submit', 27 | 'value' => 'Submit' 28 | ); 29 | 30 | register_wpas_form($form_id, $args); 31 | }); -------------------------------------------------------------------------------- /src/FieldGroup.php: -------------------------------------------------------------------------------- 1 | array(), 11 | 'relation' => Relation::_AND 12 | ); 13 | 14 | protected static $rules = array( 15 | 'fields' => 'array', 16 | 'relation' => 'Relation' 17 | ); 18 | 19 | public function __contruct(array $args = array()) { 20 | $args = self::validate($args); 21 | $this->fields = $args['fields']; 22 | $this->relation = $args['relation']; 23 | } 24 | 25 | public function addField(Field $field) { 26 | $this->fields[] = $field; 27 | } 28 | 29 | public function setRelation($relation) { 30 | $this->relation = $relation; 31 | } 32 | 33 | public function getFields() { 34 | return $this->fields; 35 | } 36 | 37 | public function getRelation() { 38 | return $this->relation; 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /src/Enum/DataType.php: -------------------------------------------------------------------------------- 1 | /'; 19 | $matches = array(); 20 | if ( !preg_match($pattern, $data_type, $matches) ) { 21 | return false; 22 | } 23 | return ( (count($matches) == 2) ? $matches[1] : false ); 24 | } 25 | 26 | public static function isValidValue($value) { 27 | if ($inner_type = self::isArrayType($value)) { 28 | return parent::isValidValue($inner_type); 29 | } 30 | return parent::isValidValue($value); 31 | } 32 | } -------------------------------------------------------------------------------- /tests/TestHttpRequest.php: -------------------------------------------------------------------------------- 1 | 5, 11 | 'two' => 'two', 12 | 'three' => array('red','white','blue'), 13 | 'four' => '' 14 | ); 15 | 16 | $request = new HttpRequest($vars); 17 | 18 | $this->assertTrue($request->get('one') == "".$vars['one']); 19 | $this->assertTrue($request->get('two') == "".$vars['two']); 20 | $this->assertTrue($request->get('three') == $vars['three']); 21 | } 22 | 23 | public function testSanitizeScriptTags() { 24 | $vars = array( 25 | 'four' => '' 26 | ); 27 | 28 | $request = new HttpRequest($vars); 29 | 30 | $this->assertTrue($request->get('four') == "alert("Hello!")"); 31 | } 32 | } -------------------------------------------------------------------------------- /src/Helper/Pagination.php: -------------------------------------------------------------------------------- 1 | $base, 19 | 'format' => 'page/%#%', 20 | 'current' => max(1, get_query_var('paged')), 21 | 'total' => $query_object->max_num_pages 22 | ); 23 | 24 | $args = self::parseArgs($args, $defaults); 25 | $output = self::createOutput($query_object, $args); 26 | 27 | return $output; 28 | } 29 | 30 | private static function createOutput($query_object, $args) { 31 | $output = ""; 32 | 33 | if ($query_object->max_num_pages > 1) { 34 | $output .= ''; 37 | } 38 | 39 | return $output; 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /src/StdObject.php: -------------------------------------------------------------------------------- 1 | fails()) { 19 | $errors = $validation->getErrors(); 20 | $err_msg = self::validationErrorMsg($errors); 21 | throw new \Exception($err_msg); 22 | } 23 | return $validation->getArgs(); 24 | } 25 | 26 | protected static function validationErrorMsg( array $errors ) { 27 | $err_msg = 'Validation of object '. get_called_class() . 28 | ' failed. '. implode(" ",$errors); 29 | return $err_msg; 30 | } 31 | 32 | protected static function parseArgs(array $args, array $defaults) { 33 | if ( is_object( $args ) ) 34 | $args = get_object_vars( $args ); 35 | else 36 | $args =& $args; 37 | 38 | if ( is_array( $defaults ) ) 39 | return array_merge( $defaults, $args ); 40 | return $args; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/Enum/BasicEnum.php: -------------------------------------------------------------------------------- 1 | getConstants(); 18 | } 19 | return self::$constCacheArray[$calledClass]; 20 | } 21 | 22 | public static function isValidName($name, $strict = false) { 23 | $constants = self::getConstants(); 24 | 25 | if ($strict) { 26 | return array_key_exists($name, $constants); 27 | } 28 | 29 | $keys = array_map('strtolower', array_keys($constants)); 30 | return in_array(strtolower($name), $keys); 31 | } 32 | 33 | public static function isValidValue($value) { 34 | $values = array_values(self::getConstants()); 35 | return in_array($value, $values, $strict = true); 36 | } 37 | 38 | public static function isValid($name, $strict = false) { 39 | return (self::isValidName($name, $strict) || self::isValidValue($name)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Helper/ResultsRange.php: -------------------------------------------------------------------------------- 1 | '', 12 | 'marker' => '-', 13 | 'post' => '' 14 | ); 15 | 16 | $args = self::parseArgs($args, $defaults); 17 | $range = self::computeRange($query_object, $args['marker']); 18 | 19 | return sprintf('%s %s %s', $args['pre'], $range, $args['post']); 20 | } 21 | 22 | private static function computeRange($query_object, $marker) { 23 | if ($query_object->found_posts < 1) return 0; 24 | 25 | $query = $query_object->query; 26 | $posts_per_page = (!empty($query['posts_per_page'])) ? $query['posts_per_page'] : get_option('posts_per_page'); 27 | 28 | $current_page = (empty($query['paged'])) ? get_query_var('paged') : $query['paged']; 29 | 30 | if ($posts_per_page <= 1) return $current_page; 31 | 32 | $low = 1 + (($current_page - 1)*$posts_per_page); 33 | $high = $low + ($posts_per_page - 1); 34 | 35 | $range = sprintf('%d%s%d', $low, $marker, $high); 36 | if ($low >= $query_object->found_posts) { 37 | $range = $query_object->found_posts; 38 | } 39 | 40 | return $range; 41 | } 42 | } -------------------------------------------------------------------------------- /src/Parser/AuthorArgParser.php: -------------------------------------------------------------------------------- 1 | '', 9 | 'format' => 'select', 10 | 'authors' => array() 11 | ); 12 | 13 | public static function parse(array $args) { 14 | $args = self::parseArgs($args, self::$defaults); 15 | $author_ids = $args['authors']; 16 | 17 | if (count($author_ids) < 1) { // get all authors 18 | $args['values'] = self::getAllAuthors(); 19 | } else { 20 | $args['values'] = self::customAuthorsList($author_ids); 21 | } 22 | 23 | return $args; 24 | } 25 | 26 | private static function getAllAuthors() { 27 | $authors_list = array(); 28 | $authors = get_users(array('who' => 'authors')); 29 | foreach ($authors as $author) { 30 | $authors_list[$author->ID] = $author->display_name; 31 | } 32 | return $authors_list; 33 | } 34 | 35 | private static function customAuthorsList($author_ids) { 36 | $authors_list = array(); 37 | foreach ($author_ids as $author) { 38 | if (get_userdata($author)) { 39 | $user = get_userdata($author); 40 | $authors_list[$author] = $user->display_name; 41 | } 42 | } 43 | return $authors_list; 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /tests/TestMetaQuery.php: -------------------------------------------------------------------------------- 1 | 'meta_key', 10 | 'label' => 'Color', 11 | 'format' => 'multi-select', 12 | 'meta_key' => 'color', 13 | 'compare' => 'LIKE', 14 | 'data_type' => 'ARRAY', 15 | 'relation' => 'AND', 16 | 'default' => 'red', 17 | 'values' => array( 18 | 'red' => 'Red', 19 | 'blue' => 'Blue', 20 | 'green' => 'Green', 21 | 'orange' => 'Orange', 22 | 'purple' => 'Purple', 23 | 'yellow' => 'Yellow' 24 | )); 25 | $fields = array(); 26 | $fields[] = new Field($args); 27 | 28 | $request = new HttpRequest( array('meta_color' => array('red')) ); 29 | $meta_query_obj = new MetaQuery($fields, 'AND', $request); 30 | $meta_query = $meta_query_obj->getQuery(); 31 | $this->assertFalse(empty($meta_query)); 32 | $this->assertTrue(count($meta_query) == 1); 33 | $this->assertFalse(empty($meta_query[0])); 34 | $this->assertTrue($meta_query[0]['value'] == 'red'); 35 | $this->assertTrue($meta_query[0]['compare'] == 'LIKE'); 36 | $this->assertTrue($meta_query[0]['type'] == 'CHAR'); 37 | $this->assertTrue($meta_query[0]['key'] == 'color'); 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /init.php: -------------------------------------------------------------------------------- 1 | admin_url( 'admin-ajax.php' ) ) ); 44 | } 45 | add_action('wp_enqueue_scripts', 'wpas_scripts'); 46 | 47 | /** 48 | * Set AJAX Handler 49 | */ 50 | function wpas_ajax_load() { 51 | echo wpas_build_ajax_response($_POST); 52 | wp_die(); 53 | } 54 | add_action( 'wp_ajax_nopriv_wpas_ajax_load', 'wpas_ajax_load' ); 55 | add_action( 'wp_ajax_wpas_ajax_load', 'wpas_ajax_load' ); -------------------------------------------------------------------------------- /src/AjaxConfig.php: -------------------------------------------------------------------------------- 1 | 'bool', 16 | 'loading_img' => 'string', 17 | 'button_text' => 'string', 18 | 'show_default_results' => 'bool', 19 | 'results_template' => 'string', 20 | 'url_hash' => 'string' 21 | ); 22 | 23 | static protected $defaults = array( 24 | 'enabled' => false, 25 | 'button_text' => 'LOAD MORE RESULTS', 26 | 'show_default_results' => true, 27 | 'url_hash' => 'results' 28 | ); 29 | 30 | function __construct($args = array()) 31 | { 32 | $args = $this->parseArgs($args, self::$defaults); 33 | $args = self::validate($args); 34 | $args = $this->processArgs($args); 35 | $this->args = $args; 36 | 37 | foreach ($this->args as $key => $value) { 38 | $this->$key = $value; 39 | } 40 | 41 | } 42 | 43 | private function processArgs(array $args) { 44 | $dir = basename(dirname(dirname(__FILE__))); 45 | 46 | if (empty($args['loading_img'])) { 47 | $args['loading_img'] = get_wpas_uri() . '/img/loading.gif'; 48 | } 49 | 50 | if (empty($args['results_template'])) { 51 | $args['results_template'] = $dir . '/templates/template-ajax-results.php'; 52 | } 53 | 54 | return $args; 55 | } 56 | 57 | public function isEnabled() 58 | { 59 | return $this->enabled; 60 | } 61 | 62 | public function loadingImage() 63 | { 64 | return $this->loading_img; 65 | } 66 | 67 | public function buttonText() 68 | { 69 | return $this->button_text; 70 | } 71 | 72 | public function resultsTemplate() 73 | { 74 | return $this->results_template; 75 | } 76 | 77 | public function showDefaultResults() 78 | { 79 | return $this->show_default_results; 80 | } 81 | 82 | public function urlHash() { 83 | return $this->url_hash; 84 | } 85 | 86 | } -------------------------------------------------------------------------------- /tests/TestField.php: -------------------------------------------------------------------------------- 1 | 'meta_key', 12 | 'format' => '', 13 | 'meta_key' => 'color', 14 | 'relation' => 'AND', 15 | 'values' => array('red' => 'Red', 'blue' => 'Blue', 'green' => 'Green'), 16 | ); 17 | 18 | $f = new Field($args); 19 | $inputs = $f->getInputs(); 20 | $this->assertTrue(count($inputs) == 1); 21 | $this->assertTrue(!empty($inputs['color'])); 22 | $this->assertTrue($f->getRelation() == 'AND'); 23 | 24 | } 25 | 26 | public function testCanBuildMultiInput() { 27 | $meta_key = 'price'; 28 | $args = array( 29 | 'type' => 'meta_key', 30 | 'meta_key' => $meta_key, 31 | 'compare' => 'BETWEEN', 32 | 'data_type' => 'NUMERIC', 33 | 'group_method' => 'merge', 34 | 'inputs' => array( 35 | array( 36 | 'format' => 'text', 37 | ), 38 | array( 39 | 'format' => 'text' 40 | ) 41 | ) 42 | ); 43 | 44 | $f = new Field($args); 45 | $inputs = $f->getInputs(); 46 | $this->assertTrue(count($inputs) == 2); 47 | $this->assertTrue($f->getCompare() == 'BETWEEN'); 48 | $this->assertTrue($f->getFieldId() == $meta_key); 49 | } 50 | 51 | public function testCanOverrideInvalidRelation() { 52 | $args = array( 53 | 'type' => 'taxonomy', 54 | 'taxonomy' => 'category', 55 | 'relation' => 'IN' 56 | ); 57 | $f = new Field($args); 58 | $defaults = $f->getDefaults(); 59 | $default_relation = $defaults['relation']; 60 | $this->assertTrue($f->getRelation() == $default_relation); 61 | } 62 | 63 | /** 64 | * @expectedException Exception 65 | */ 66 | public function testThrowsExceptionOnMissingType() { 67 | $args = array( 68 | 'taxonomy' => 'category', 69 | 'relation' => 'IN' 70 | ); 71 | $f = new Field($args); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/DateQuery.php: -------------------------------------------------------------------------------- 1 | query = $this->build($field, $request); 12 | } 13 | 14 | public function build($field, $request) { 15 | $query = array(); 16 | $this->request = $request; 17 | 18 | if (empty($field)) return $query; 19 | $inputs = $field->getInputs(); 20 | 21 | if (empty($inputs)) return $query; 22 | 23 | // Temporary restriction (v1.4), allow only one input for date 24 | // TODO: Expand functionality to support multiple inputs 25 | $input = reset($inputs); 26 | 27 | 28 | switch($input['date_type']) { 29 | case 'year': 30 | $query = $this->addYear($query, $this->request); 31 | break; 32 | case 'month' : 33 | $query = $this->addMonth($query, $this->request); 34 | break; 35 | case 'day' : 36 | $query = $this->addDay($query, $this->request); 37 | break; 38 | } 39 | 40 | return $query; 41 | } 42 | 43 | private function addYear($query, $request) { 44 | $var = RequestVar::date_year; 45 | $value = $this->getRequestVar($var, $request); 46 | if ($value == false) return $query; 47 | 48 | $query['year'] = $value; 49 | 50 | return $query; 51 | } 52 | 53 | private function addMonth($query, $request) { 54 | $var = RequestVar::date_month; 55 | $value = $this->getRequestVar($var, $request); 56 | if ($value == false) return $query; 57 | 58 | $value = explode('-',$value); 59 | if(count($value) != 2) return $query; 60 | 61 | $query['year'] = $value[0]; 62 | $query['month'] = $value[1]; 63 | 64 | return $query; 65 | } 66 | 67 | private function addDay($query, $request) { 68 | $var = RequestVar::date_day; 69 | $value = $this->getRequestVar($var, $request); 70 | if ($value == false) return $query; 71 | 72 | $value = explode('-', $value); 73 | if(count($value) != 3) return $query; 74 | 75 | $query['year'] = $value[0]; 76 | $query['month'] = $value[1]; 77 | $query['day'] = $value[2]; 78 | 79 | return $query; 80 | } 81 | 82 | private function getRequestVar($var, $request) { 83 | $val = $request->get($var, null); 84 | if ($val === null) return false; 85 | if (is_array($val)) implode(",",$val); 86 | return $val; 87 | } 88 | 89 | 90 | /** 91 | * @return mixed 92 | */ 93 | public function getQuery() 94 | { 95 | return $this->query; 96 | } 97 | 98 | } -------------------------------------------------------------------------------- /src/TaxQuery.php: -------------------------------------------------------------------------------- 1 | request = $request; 13 | $this->query = $this->build($fields, $relation, $request); 14 | } 15 | 16 | /** 17 | * Build and return a tax_query argument array 18 | * 19 | * @param array $fields 20 | * @param string $relation 21 | * @param $request 22 | * @return array 23 | */ 24 | public function build(array $fields, $relation = 'AND', $request) { 25 | $query = array(); 26 | 27 | if (empty($fields)) return $query; 28 | foreach ($fields as $field) { 29 | $group = $this->taxQueryGroup($field, $request); 30 | if (!empty($group)) $query[] = $group; 31 | } 32 | 33 | if (count($query) > 1) { 34 | $query['relation'] = $relation; 35 | } 36 | 37 | return $query; 38 | } 39 | 40 | /** 41 | * 42 | * Build a tax_query group comprising query arguments and values related 43 | * to a single taxonomy field 44 | * 45 | * @param $field 46 | * @return array 47 | */ 48 | public function taxQueryGroup($field) { 49 | $group = array(); 50 | $taxonomy = $field->getFieldId(); 51 | $inputs = $field->getInputs(); 52 | 53 | foreach ($inputs as $input_name => $input) { 54 | $clause = $this->taxQueryClause($taxonomy, $input_name, $input); 55 | if (!empty($clause)) $group[] = $clause; 56 | } 57 | 58 | if (count($group) > 1) { 59 | $group['relation'] = $field->getRelation(); 60 | } else if (count($group) == 1) { 61 | $group = $group[0]; 62 | } 63 | return $group; 64 | } 65 | 66 | /** 67 | * Build a tax_query clause corresponding to a single input 68 | * 69 | * @param $taxonomy 70 | * @param $input_name 71 | * @param $input 72 | * @return array 73 | */ 74 | private function taxQueryClause($taxonomy, $input_name, $input) { 75 | if (empty($input)) return; 76 | $request_var = RequestVar::nameToVar($input_name, 'taxonomy'); 77 | $terms = $this->request->get($request_var); 78 | 79 | if (empty($terms)) return; 80 | 81 | $clause = array(); 82 | $clause['taxonomy'] = $taxonomy; 83 | $clause['operator'] = $input['operator']; 84 | $clause['field'] = (empty($inputs['term_format'])) ? self::$default_format : $inputs['term_format']; 85 | $clause['terms'] = $terms; 86 | 87 | return $clause; 88 | } 89 | 90 | /** 91 | * @return mixed 92 | */ 93 | public function getQuery() 94 | { 95 | return $this->query; 96 | } 97 | 98 | } -------------------------------------------------------------------------------- /lib.php: -------------------------------------------------------------------------------- 1 | query(); 33 | $template = $wpas->get_ajax()->resultsTemplate(); 34 | 35 | $response = array(); 36 | $response["results"] = wpas_load_template_part($template, $q); 37 | $response["current_page"] = $q->query_vars['paged']; 38 | $response["max_page"] = $q->max_num_pages; 39 | 40 | if ($response["results"] === false) { 41 | $wpas->set_error("AJAX results template '".$template."' not found in theme root."); 42 | } 43 | 44 | $response["debug"] = ""; 45 | if ($wpas->debug_enabled()) $response["debug"] = "
". $wpas->create_debug_output() . "
"; 46 | 47 | return json_encode($response); 48 | } 49 | 50 | /** 51 | * Loads and returns a template part 52 | * 53 | * Used with AJAX requests to build the list of search results 54 | * 55 | * @param $template 56 | * @param $query_object 57 | * @return string 58 | */ 59 | function wpas_load_template_part($template, $query_object) { 60 | global $wp_query; 61 | 62 | $template_suffix = '/'.ltrim($template,'/'); 63 | $template = get_stylesheet_directory().$template_suffix; 64 | if (!file_exists($template)) { 65 | $template = get_template_directory().$template_suffix; 66 | if (!file_exists($template)) return false; 67 | } 68 | $temp = $wp_query; 69 | $wp_query = $query_object; 70 | 71 | ob_start(); 72 | load_template($template); 73 | $var = ob_get_contents(); 74 | ob_end_clean(); 75 | 76 | $wp_query = $temp; 77 | return $var; 78 | } 79 | 80 | /** 81 | * Registers a search form, making it available for use in templates 82 | * 83 | * @param $name Unique identifier for the search form 84 | * @param $args The form's configuration arguments 85 | */ 86 | function register_wpas_form($name, $args) { 87 | global $WPAS_FORMS; 88 | if (!is_array($args)) return; 89 | $args["wpas_id"] = $name; 90 | $WPAS_FORMS[$name] = $args; 91 | } 92 | 93 | /** 94 | * Deregisters a search form, making it unavailable for use 95 | * 96 | * @param $name Unique identifier for the search form 97 | * @return bool True if form successfully deregistered, false if form did not exist 98 | */ 99 | function deregister_wpas_form($name) { 100 | global $WPAS_FORMS; 101 | if (!isset($WPAS_FORMS[$name])) return false; 102 | unset($WPAS_FORMS[$name]); 103 | return true; 104 | } -------------------------------------------------------------------------------- /src/Parser/TaxonomyArgParser.php: -------------------------------------------------------------------------------- 1 | '', 10 | 'format' => 'select', 11 | 'term_format' => 'slug', 12 | 'operator' => 'AND', 13 | 'hide_empty' => false, 14 | 'terms' => array(), 15 | 'nested' => false, 16 | 'taxonomy' => null, 17 | 'term_args' => array() 18 | ); 19 | 20 | protected static $term_defaults = array( 21 | 'hide_empty' => false 22 | ); 23 | 24 | public static function parse(array $args) { 25 | $args = self::parseArgs($args, self::$defaults); 26 | 27 | $the_tax = get_taxonomy($args['taxonomy']); 28 | 29 | if (!is_object($the_tax)) { 30 | $msg = "Taxonomy '". $args['taxonomy'] ."' not found in this WordPress 31 | installation."; 32 | throw new \Exception($msg); 33 | } 34 | 35 | $args = self::setTermArgs($args); 36 | $format = $args['format']; 37 | 38 | if (!empty($args['values']) || $format == 'text' || $format == 'textarea') { 39 | return $args; 40 | } 41 | 42 | $args = self::addTermsList($args); 43 | 44 | return $args; 45 | } 46 | 47 | private static function setTermArgs($args) { 48 | if (isset($args['term_args']) && is_array($args['term_args'])) { 49 | $args['term_args'] = self::parseArgs($args['term_args'], self::$term_defaults); 50 | } 51 | return $args; 52 | } 53 | 54 | private static function addTermsList($args) { 55 | extract($args); 56 | if (isset($terms) && is_array($terms) && (count($terms) < 1)) { 57 | // No terms specified; populate with all terms 58 | $args['values'] = self::allTermsList($taxonomy, $term_args, $term_format, $nested); 59 | } else { // Custom term list 60 | $args['nested'] = false; // Disallow nesting for custom term lists 61 | $args['values'] = self::customTermsList($terms, $taxonomy, $term_format); 62 | } 63 | return $args; 64 | } 65 | 66 | private static function allTermsList($taxonomy, $term_args, $term_format, $nested) { 67 | $walker = new TermsWalker(array('taxonomy' => $taxonomy, 68 | 'term_format' => $term_format), 69 | $term_args); 70 | if ($nested) { 71 | return $walker->build_nested_terms_array(0); 72 | } 73 | return $walker->build_basic_terms_array(); 74 | } 75 | 76 | private static function customTermsList($terms, $taxonomy, $term_format) { 77 | $term_objects = array(); 78 | $term_values = array(); 79 | foreach ($terms as $term_identifier) { 80 | $term = get_term_by($term_format, $term_identifier, $taxonomy); 81 | if ($term) { 82 | $term_objects[] = $term; 83 | } 84 | } 85 | foreach ($term_objects as $term) { 86 | $term_values[self::termValue($term,$term_format)] = $term->name; 87 | } 88 | return $term_values; 89 | } 90 | 91 | private static function termValue($term, $format) { 92 | switch(strtolower($format)) { 93 | case 'id' : 94 | return $term->term_id; 95 | case 'name' : 96 | return $term->name; 97 | default : 98 | return $term->slug; 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /tests/TestForm.php: -------------------------------------------------------------------------------- 1 | 'http://google.com', 10 | 'method' => 'GET', 11 | 'id' => 'my_id', 12 | 'name' => 'some_name', 13 | 'class' => array('form-class') ); 14 | 15 | $form = new Form("default", $args); 16 | $attr = $form->getAttributes(); 17 | $this->assertEquals($attr['action'], 'http://google.com'); 18 | $this->assertEquals($attr['method'], 'GET'); 19 | $this->assertEquals($attr['name'], 'some_name'); 20 | $this->assertEquals($attr['class'], array('form-class')); 21 | 22 | } 23 | 24 | public function testCanAddInput() { 25 | $args = array( 26 | 'action' => 'http://google.com', 27 | 'method' => 'GET', 28 | 'id' => 'my_id', 29 | 'name' => 'some_name', 30 | 'class' => array('form-class') ); 31 | 32 | $form = new Form("default", $args); 33 | $input_args = array('field_type' => 'meta_key', 'format' => 'checkbox', 'values' => array('one', 'two')); 34 | $input = new Input("myinput", $input_args); 35 | 36 | $form->addInput($input); 37 | } 38 | 39 | public function testCanGetInput() { 40 | $args = array( 41 | 'action' => 'http://google.com', 42 | 'method' => 'GET', 43 | 'id' => 'my_id', 44 | 'name' => 'some_name', 45 | 'class' => array('form-class') ); 46 | 47 | $form = new Form("default", $args); 48 | $input_args = array('field_type' => 'meta_key', 'format' => 'checkbox', 'values' => array('one', 'two')); 49 | $input = new Input("myinput", $input_args); 50 | 51 | $form->addInput($input); 52 | $inputs = $form->getInputs(); 53 | $this->assertTrue(is_array($inputs) && count($inputs) == 1); 54 | $this->assertTrue($inputs[0] instanceof Input); 55 | $this->assertTrue($inputs[0]->getInputName() == "myinput"); 56 | } 57 | 58 | public function testToHTML() { 59 | $args = array( 60 | 'action' => 'http://google.com', 61 | 'method' => 'GET', 62 | 'id' => 'my_id', 63 | 'name' => 'some_name', 64 | 'class' => array('form-class') ); 65 | 66 | $form = new Form("default", $args); 67 | 68 | $input_args = array('field_type' => 'meta_key', 'format' => 'checkbox', 'values' => array('one', 'two')); 69 | $input = new Input("myinput", $input_args); 70 | $form->addInput($input); 71 | 72 | $input_args = array('field_type' => 'search', 'format' => 'text', 'placeholder' => 'Enter keywords...'); 73 | $input = new Input("myinput", $input_args); 74 | $form->addInput($input); 75 | 76 | $this->assertTrue(is_string($form->toHTML())); 77 | } 78 | 79 | 80 | public function testBadMethodInvokesDefault() { 81 | $args = array( 'method' => 'BADMETHOD' ); 82 | $form = new Form("default", $args); 83 | $defaults = $form->getDefaults(); 84 | $attr = $form->getAttributes(); 85 | $this->assertTrue($attr['method'] == $defaults['method']); 86 | } 87 | 88 | 89 | public function testBadNameInvokesDefault() { 90 | $args = array( 'name' => 1.2 ); 91 | $form = new Form("default", $args); 92 | $defaults = $form->getDefaults(); 93 | $attr = $form->getAttributes(); 94 | $this->assertTrue($attr['name'] == $defaults['name']); 95 | } 96 | 97 | public function testBadClassInvokesDefault() { 98 | $args = array( 'class' => array(1,2,3) ); 99 | $form = new Form("default", $args); 100 | $defaults = $form->getDefaults(); 101 | $attr = $form->getAttributes(); 102 | $this->assertTrue($attr['class'] == $defaults['class']); 103 | } 104 | 105 | } -------------------------------------------------------------------------------- /bin/install-wp-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ $# -lt 3 ]; then 4 | echo "usage: $0 [db-host] [wp-version]" 5 | exit 1 6 | fi 7 | 8 | DB_NAME=$1 9 | DB_USER=$2 10 | DB_PASS=$3 11 | DB_HOST=${4-localhost} 12 | WP_VERSION=${5-latest} 13 | 14 | WP_TESTS_DIR=${WP_TESTS_DIR-/tmp/wordpress-tests-lib} 15 | WP_CORE_DIR=${WP_CORE_DIR-/tmp/wordpress/} 16 | 17 | download() { 18 | if [ `which curl` ]; then 19 | curl -s "$1" > "$2"; 20 | elif [ `which wget` ]; then 21 | wget -nv -O "$2" "$1" 22 | fi 23 | } 24 | 25 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+(\.[0-9]+)? ]]; then 26 | WP_TESTS_TAG="tags/$WP_VERSION" 27 | elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 28 | WP_TESTS_TAG="trunk" 29 | else 30 | # http serves a single offer, whereas https serves multiple. we only want one 31 | download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json 32 | grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json 33 | LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') 34 | if [[ -z "$LATEST_VERSION" ]]; then 35 | echo "Latest WordPress version could not be found" 36 | exit 1 37 | fi 38 | WP_TESTS_TAG="tags/$LATEST_VERSION" 39 | fi 40 | 41 | set -ex 42 | 43 | install_wp() { 44 | 45 | if [ -d $WP_CORE_DIR ]; then 46 | return; 47 | fi 48 | 49 | mkdir -p $WP_CORE_DIR 50 | 51 | if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 52 | mkdir -p /tmp/wordpress-nightly 53 | download https://wordpress.org/nightly-builds/wordpress-latest.zip /tmp/wordpress-nightly/wordpress-nightly.zip 54 | unzip -q /tmp/wordpress-nightly/wordpress-nightly.zip -d /tmp/wordpress-nightly/ 55 | mv /tmp/wordpress-nightly/wordpress/* $WP_CORE_DIR 56 | else 57 | if [ $WP_VERSION == 'latest' ]; then 58 | local ARCHIVE_NAME='latest' 59 | else 60 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 61 | fi 62 | download https://wordpress.org/${ARCHIVE_NAME}.tar.gz /tmp/wordpress.tar.gz 63 | tar --strip-components=1 -zxmf /tmp/wordpress.tar.gz -C $WP_CORE_DIR 64 | fi 65 | 66 | download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php 67 | } 68 | 69 | install_test_suite() { 70 | # portable in-place argument for both GNU sed and Mac OSX sed 71 | if [[ $(uname -s) == 'Darwin' ]]; then 72 | local ioption='-i .bak' 73 | else 74 | local ioption='-i' 75 | fi 76 | 77 | # set up testing suite if it doesn't yet exist 78 | if [ ! -d $WP_TESTS_DIR ]; then 79 | # set up testing suite 80 | mkdir -p $WP_TESTS_DIR 81 | svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes 82 | fi 83 | 84 | if [ ! -f wp-tests-config.php ]; then 85 | download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php 86 | sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR':" "$WP_TESTS_DIR"/wp-tests-config.php 87 | sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php 88 | sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php 89 | sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php 90 | sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php 91 | fi 92 | 93 | } 94 | 95 | install_db() { 96 | # parse DB_HOST for port or socket references 97 | local PARTS=(${DB_HOST//\:/ }) 98 | local DB_HOSTNAME=${PARTS[0]}; 99 | local DB_SOCK_OR_PORT=${PARTS[1]}; 100 | local EXTRA="" 101 | 102 | if ! [ -z $DB_HOSTNAME ] ; then 103 | if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then 104 | EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" 105 | elif ! [ -z $DB_SOCK_OR_PORT ] ; then 106 | EXTRA=" --socket=$DB_SOCK_OR_PORT" 107 | elif ! [ -z $DB_HOSTNAME ] ; then 108 | EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" 109 | fi 110 | fi 111 | 112 | # create database 113 | mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA 114 | } 115 | 116 | install_wp 117 | install_test_suite 118 | install_db -------------------------------------------------------------------------------- /src/DateInputBuilder.php: -------------------------------------------------------------------------------- 1 | '', 41 | 'format' => 'select', 42 | 'date_type' => $default_date_type, 43 | 'date_format' => false, 44 | 'values' => array() ); 45 | $disallowed_formats = array('multi-select', 'checkbox'); 46 | 47 | $args = self::parseArgs($args, $defaults); 48 | 49 | if (in_array($args['format'], $disallowed_formats)) { 50 | throw new \Exception("Date field does not currently support multi-select or checkbox formats"); 51 | } 52 | 53 | return $args; 54 | } 55 | 56 | /** 57 | * Add auto-generated list of dates to an arguments array 58 | * 59 | * @param $args 60 | * @param array $post_types 61 | * @return mixed 62 | */ 63 | private static function addDateValues($args, array $post_types) { 64 | if (!empty($args['values'])) return $args; 65 | 66 | $date_format = (empty($args['date_format'])) ? false : $args['date_format']; 67 | $date_type = (empty($args['date_type'])) ? 'year' : $args['date_type']; 68 | 69 | $args['values'] = self::getDates($date_type, $date_format, $post_types); 70 | return $args; 71 | } 72 | 73 | 74 | /** 75 | * Returns an array of dates in which content has been published 76 | * 77 | * @param string $date_type 78 | * @param bool $format 79 | * @param array $post_types 80 | * @return array 81 | */ 82 | private static function getDates($date_type = 'year', $format = false, array $post_types) { 83 | $display_format = ($format) ? $format : self::dateFormat('display', $date_type); 84 | $compare_format = self::dateFormat('compare', $date_type); 85 | 86 | $post_status = 'publish'; 87 | $posts = get_posts(array('numberposts' => 1000, 'post_type' => $post_types, 'post_status' => $post_status)); 88 | 89 | $previous_value = ""; 90 | $dates = array(); 91 | 92 | foreach($posts as $post) { 93 | $post_date = strtotime($post->post_date); 94 | $current_display = date_i18n($display_format, $post_date); 95 | $current_value = date($compare_format, $post_date); 96 | if ($previous_value != $current_value) { 97 | $dates[$current_value] = $current_display; 98 | } 99 | $previous_value = $current_value; 100 | 101 | } 102 | return $dates; 103 | } 104 | 105 | 106 | private static function dateFormat($format, $date_type) { 107 | $map = array( 108 | 'display' => array('year' => 'Y', 'month' =>'M Y', 'day' => 'M j, Y'), 109 | 'compare' => array('year' => 'Y', 'month' =>'Y-m', 'day' => 'Y-m-d') 110 | ); 111 | 112 | return (isset($map[$format][$date_type])) ? $map[$format][$date_type] : reset($map[$format]); 113 | } 114 | 115 | } -------------------------------------------------------------------------------- /src/Enum/RequestVar.php: -------------------------------------------------------------------------------- 1 | 's', 21 | 'post_type' => 'post_type', 22 | 'author' => 'author', 23 | 'meta_key' => 'meta_query', 24 | 'taxonomy' => 'tax_query', 25 | 'date' => 'date_query' 26 | ); 27 | 28 | /** 29 | * Given a request variable name, returns the name (value) of that 30 | * variable. Returns false if not a valid request var. 31 | * 32 | * e.g. 'meta_color' ==> 'color' 33 | * 34 | * @param $value 35 | * @return bool|string 36 | */ 37 | public static function varToName($value) { 38 | $values = array_values(self::getConstants()); 39 | 40 | if (self::isMetaVar($value) || self::isDateVar($value)) { 41 | return (strlen($value > 5))? substr($value, 5) : false; 42 | } 43 | 44 | if (self::isTaxVar($value)) { 45 | return (strlen($value > 4))? substr($value, 4) : false; 46 | } 47 | 48 | if (in_array($value, $values, $strict = true)) { 49 | return $value; 50 | } 51 | 52 | return false; 53 | } 54 | 55 | /** 56 | * Given an input name and optional field type, returns the corresponding 57 | * request parameter name used by WPAS. 58 | * 59 | * e.g. color, meta_key ==> 'meta_color' 60 | * 61 | * @param $input_name 62 | * @param $field_type 63 | * @return string 64 | */ 65 | public static function nameToVar($input_name, $field_type = false, $date_type = false) { 66 | switch($field_type) { 67 | case 'meta_key' : 68 | return self::meta_key . $input_name; 69 | case 'taxonomy' : 70 | return self::taxonomy . $input_name; 71 | case 'date' : 72 | if (!$date_type) return self::date_year; 73 | return self::date . self::dateTypeSuffix($date_type); 74 | } 75 | 76 | if (self::isValidName($input_name)) return constant( 'self::'. $input_name ); 77 | return $input_name; 78 | } 79 | 80 | /** 81 | * Given a request var, returns the equivalent variable name for WP_Query. 82 | * Returns false if not a valid request var. 83 | * 84 | * @param RequestVar $request_var 85 | * @return bool|string 86 | */ 87 | public static function wpQueryVar($request_var) { 88 | if (!self::isValidName($request_var)) return false; 89 | 90 | if (!empty(self::$wp_query_vars[$request_var])) { 91 | return self::$wp_query_vars[$request_var]; 92 | } 93 | 94 | return constant( 'self::'. $request_var ); 95 | } 96 | 97 | private static function dateTypeSuffix($date_type) { 98 | switch($date_type) { 99 | case 'month' : 100 | return 'm'; 101 | case 'day' : 102 | return 'd'; 103 | default : 104 | return 'y'; 105 | } 106 | } 107 | 108 | public static function isValidValue($value) { 109 | return self::varToName($value); 110 | } 111 | 112 | public static function isValidDateVar($value) { 113 | $types = array('date_y'=>1,'date_m'=>1,'date_d'=>1); 114 | return (isset($types[$value])); 115 | } 116 | 117 | public static function isTaxVar($value) { 118 | return (substr($value, 0, 4) == self::taxonomy); 119 | } 120 | 121 | public static function isMetaVar($value) { 122 | return (substr($value, 0, 5) == self::meta_key); 123 | } 124 | 125 | public static function isDateVar($value) { 126 | return (substr($value, 0, 5) == self::date); 127 | } 128 | 129 | 130 | 131 | } -------------------------------------------------------------------------------- /tests/TestType.php: -------------------------------------------------------------------------------- 1 | assertTrue(Type::isValidName($type)); 19 | } 20 | 21 | } 22 | 23 | public function testFailsOnInvalidTypes() { 24 | $this->assertFalse(Type::isValidName("badtype")); 25 | $this->assertFalse(Type::isValidName(1)); 26 | $this->assertFalse(Type::isValidName(array("badtype"))); 27 | } 28 | 29 | 30 | public function testPassesOnValidArrayTypes() { 31 | $types = array( 32 | "string", 33 | "numeric", 34 | "bool", 35 | "scalar", 36 | "array", 37 | "object" 38 | ); 39 | 40 | foreach ($types as $type) { 41 | $this->assertTrue(Type::isValidName("array<".$type.">")); 42 | } 43 | } 44 | 45 | public function testFailsOnInvalidArrayType() { 46 | $this->assertFalse(Type::isValidName("array")); 47 | } 48 | 49 | public function testTypeSubsets() { 50 | $this->assertTrue(Type::isSubtypeOf("string", "string")); 51 | $this->assertTrue(Type::isSubtypeOf("string", "scalar")); 52 | $this->assertTrue(Type::isSubtypeOf("numeric", "scalar")); 53 | $this->assertTrue(Type::isSubtypeOf("bool", "scalar")); 54 | 55 | $this->assertFalse(Type::isSubtypeOf("array", "scalar")); 56 | $this->assertFalse(Type::isSubtypeOf("object", "scalar")); 57 | 58 | $this->assertTrue(Type::isSubtypeOf("array", "array")); 59 | $this->assertTrue(Type::isSubtypeOf("array", "array")); 60 | $this->assertTrue(Type::isSubtypeOf("array", "array")); 61 | $this->assertTrue(Type::isSubtypeOf("array", "array")); 62 | 63 | $this->assertFalse(Type::isSubtypeOf("array", "array")); 64 | $this->assertFalse(Type::isSubtypeOf("array", "array")); 65 | 66 | } 67 | 68 | public function testMatches() { 69 | 70 | $this->assertTrue(Type::matches("string", "hello")); 71 | $this->assertTrue(Type::matches("bool", true)); 72 | $this->assertTrue(Type::matches("numeric", 134)); 73 | $this->assertTrue(Type::matches("object", $this)); 74 | $this->assertTrue(Type::matches("RequestMethod", "GET")); 75 | $this->assertTrue(Type::matches("RequestMethod", "get")); 76 | $this->assertTrue(Type::matches("RequestMethod", "POST")); 77 | $this->assertTrue(Type::matches("InputFormat", "multi-select")); 78 | $this->assertTrue(Type::matches("array", array(1, "five"))); 79 | $this->assertTrue(Type::matches("array", array("one", "five"))); 80 | $this->assertTrue(Type::matches("array", array(true, false))); 81 | $this->assertTrue(Type::matches("array", array(5, 98))); 82 | $this->assertTrue(Type::matches("array", array("one", 98, false))); 83 | $this->assertTrue(Type::matches("array", array($this))); 84 | 85 | $this->assertFalse(Type::matches("string", 431)); 86 | $this->assertFalse(Type::matches("bool", "hello")); 87 | $this->assertFalse(Type::matches("numeric", array(123))); 88 | $this->assertFalse(Type::matches("array", 1)); 89 | $this->assertFalse(Type::matches("RequestMethod", "BADMETHOD")); 90 | $this->assertFalse(Type::matches("InputFormat", "notaformat")); 91 | $this->assertFalse(Type::matches("array", array(1, "five", $this))); 92 | $this->assertFalse(Type::matches("array", array("hi", false))); 93 | $this->assertFalse(Type::matches("array", array(5, "one"))); 94 | $this->assertFalse(Type::matches("array", array("one", array(98), false))); 95 | $this->assertFalse(Type::matches("array", array("one", array(98), false))); 96 | 97 | } 98 | 99 | public function testFlexibleBoolType() { 100 | $this->assertTrue(Type::matches("bool", "1")); 101 | $this->assertTrue(Type::matches("bool", "0")); 102 | $this->assertTrue(Type::matches("bool", "true")); 103 | $this->assertTrue(Type::matches("bool", "false")); 104 | } 105 | 106 | } -------------------------------------------------------------------------------- /src/TermsWalker.php: -------------------------------------------------------------------------------- 1 | 'parent', 'id' => 'term_id'); 16 | 17 | function __construct( $args = array(), $term_args ) { 18 | $defaults = array( 'taxonomy' => 'category', 19 | 'term_format' => 'slug' ); 20 | 21 | extract($this->parseArgs($args,$defaults)); 22 | 23 | $this->taxonomy = $taxonomy; 24 | $this->term_identifier = $term_format; 25 | 26 | switch($term_format) { 27 | case 'id' : 28 | case 'ID' : 29 | $this->term_identifier = 'term_id'; 30 | break; 31 | case 'Name' : 32 | case 'name' : 33 | $this->term_identifier = 'name'; 34 | break; 35 | default : 36 | $this->term_identifier = 'slug'; 37 | break; 38 | } 39 | 40 | $this->elements = get_terms($taxonomy, $term_args); 41 | 42 | } 43 | 44 | /** 45 | * Builds and returns a nested, hierarchical array of taxonomy terms 46 | * 47 | * @param int $max_depth The maximum number of nested levels to recurse 48 | * (0 == all levels, -1 == flat hierarchy) 49 | * @return array An Array of nested taxonomy terms 50 | */ 51 | public function build_nested_terms_array( $max_depth ) { 52 | $parent_field = self::$db_fields['parent']; 53 | $parent_element_array = array(); 54 | $value_array = array(); 55 | 56 | if ($max_depth < -1) //invalid parameter 57 | return $value_array; 58 | 59 | if (empty($this->elements)) //nothing to walk 60 | return $value_array; 61 | 62 | // Max depth of -1 means no hierarchy 63 | if ( -1 == $max_depth ) { 64 | return $this->build_basic_terms_array(); 65 | } 66 | 67 | $elements_table = array(0 => array()); 68 | foreach ( $this->elements as $e) { 69 | $elements_table[ $e->$parent_field ][] = $e; 70 | } 71 | 72 | foreach ( $elements_table[0] as $e ) { 73 | $this->add_element($e, $value_array, $parent_element_array, 74 | $elements_table, $max_depth, 0); 75 | } 76 | 77 | return $value_array; 78 | } 79 | 80 | 81 | /** 82 | * Adds a term element and all of its children to $value_array. 83 | * Recursively calls itself to add each of the term's children, 84 | * incrementing depth by +1 in the process. 85 | * 86 | * @param array $element Term element to be added to the array 87 | * @param array $value_array Master array to add the element to 88 | * @param array $parent_element_array Parent term of the current element 89 | * @param array $elements_table Table of all elements index by parent_id 90 | * @param int $max_depth The maximum number of nested levels to recurse 91 | * @param int $depth The current depth of recursion 92 | */ 93 | public function add_element($element, &$value_array, &$parent_element_array, 94 | &$elements_table, $max_depth, $depth) { 95 | 96 | $id_field = self::$db_fields['id']; 97 | $id = $element->$id_field; 98 | $term_identifier = $this->term_identifier; 99 | 100 | $el = array( 101 | 'value' => $element->$term_identifier, 102 | 'label' => $element->name, 103 | 'children' => array() 104 | ); 105 | 106 | $has_children = false; 107 | if (!empty($elements_table) && !empty($elements_table[$id])) { 108 | $has_children = true; 109 | } 110 | 111 | if ( ($max_depth == 0 || $max_depth > $depth+1 ) && $has_children ) { 112 | foreach($elements_table[$element->term_id] as $child) { 113 | $this->add_element($child, $value_array, $el, $elements_table, 114 | $max_depth, $depth+1); 115 | } 116 | } 117 | 118 | if (empty($parent_element_array)) { 119 | // If this is a top-level element, just append to the array 120 | $value_array[$el['value']] = $el; 121 | } else { 122 | // Otherwise append to parent's children array 123 | $parent_element_array['children'][$el['value']] = $el; 124 | } 125 | 126 | } 127 | 128 | /** 129 | * Builds and returns a basic, non-hierarchical array of taxonomy 130 | * terms. 131 | * 132 | * @return array 133 | */ 134 | public function build_basic_terms_array() { 135 | $term_values = array(); 136 | $term_identifier = $this->term_identifier; 137 | 138 | foreach ($this->elements as $term) { 139 | $term_values[$term->$term_identifier] = $term->name; 140 | } 141 | 142 | return $term_values; 143 | } 144 | 145 | } -------------------------------------------------------------------------------- /tests/TestInput.php: -------------------------------------------------------------------------------- 1 | array('form-class'), 10 | 'field_type' => 'search', 11 | 'format' => 'select', 12 | 'attributes' => array("one", "two"), 13 | 'label' => 'mylabel', 14 | 'placeholder' => 'myplaceholder', 15 | 'values' => array('one', 'three', 'four'), 16 | 'nested' => false, 17 | 'selected' => array('one'), 18 | 'pre_html' => '

Some HTML

', 19 | 'post_html' => 'more code', 20 | ); 21 | 22 | $input = new Input('my_input', $args); 23 | $input->toHTML(); 24 | $this->assertEquals($input->getInputName(), 'my_input'); 25 | $this->assertEquals($input->getClass(), 'form-class'); 26 | $this->assertEquals($input->getFieldType(), 'search'); 27 | $this->assertEquals($input->getFormat(), 'select'); 28 | $this->assertEquals($input->getAttributes(), array("one", "two")); 29 | $this->assertEquals($input->getLabel(), 'mylabel'); 30 | $this->assertEquals($input->getPlaceholder(), 'myplaceholder'); 31 | $this->assertEquals($input->getValues(), array('one', 'three', 'four')); 32 | $this->assertEquals($input->isNested(), false); 33 | $this->assertEquals($input->getSelected(), array('one')); 34 | $this->assertEquals($input->getPreHtml(), '

Some HTML

'); 35 | $this->assertEquals($input->getPostHtml(), 'more code'); 36 | } 37 | 38 | /** 39 | * @expectedException Exception 40 | */ 41 | public function testFailsOnMissingFormat() { 42 | $args = array( 43 | 'id' => 'my_id', 44 | 'name' => 'some_name', 45 | 'class' => array('form-class'), 46 | 'field_type' => 'search', 47 | 'attributes' => array("one", "two"), 48 | 'label' => 'mylabel', 49 | ); 50 | 51 | $input = new Input('my_input', $args); 52 | } 53 | 54 | /** 55 | * @expectedException InvalidArgumentException 56 | */ 57 | public function testFailsOnInvalidName() { 58 | $args = array( 59 | 'id' => 'my_id', 60 | 'name' => 'some_name', 61 | 'class' => array('form-class'), 62 | 'format' => 'select', 63 | 'field_type' => 'search', 64 | 'attributes' => array("one", "two"), 65 | 'label' => 'mylabel', 66 | ); 67 | 68 | $input = new Input(123, $args); 69 | } 70 | 71 | public function testAllowNull() { 72 | $args = array( 73 | 'field_type' => 'search', 74 | 'format' => 'select', 75 | 'values' => array('one' => 'one', 'three' => 'three', 'four' => 'four'), 76 | 'allow_null' => 'my_null_value' 77 | ); 78 | $input = new Input('my_input', $args); 79 | $values = $input->getValues(); 80 | $this->assertTrue(reset($values) == 'my_null_value'); 81 | } 82 | 83 | 84 | public function testBasicSelect() { 85 | $args = array('field_type' => 'meta_key', 'format' => 'select', 'values' => array('one', 'two')); 86 | $input = new Input("myinput", $args); 87 | $input->toHTML(); 88 | } 89 | 90 | public function testBasicCheckbox() { 91 | $args = array('field_type' => 'meta_key', 'format' => 'checkbox', 'values' => array('one', 'two')); 92 | $input = new Input("myinput", $args); 93 | $input->toHTML(); 94 | } 95 | 96 | public function testBasicRadio() { 97 | $args = array('field_type' => 'meta_key', 'format' => 'checkbox', 'values' => array('one', 'two')); 98 | $input = new Input("myinput", $args); 99 | $input->toHTML(); 100 | } 101 | 102 | public function testBasicInput() { 103 | $args = array('field_type' => 'meta_key', 'format' => 'text', 'values' => array('somevalue')); 104 | $input = new Input("myinput", $args); 105 | $input->toHTML(); 106 | } 107 | 108 | public function testBasicTextarea() { 109 | $args = array('field_type' => 'meta_key', 'format' => 'textarea', 'values' => array('somevalue')); 110 | $input = new Input("myinput", $args); 111 | $input->toHTML(); 112 | } 113 | 114 | public function testBasicSubmit() { 115 | $args = array('field_type' => 'meta_key', 'format' => 'submit', 'values' => array('Submit')); 116 | $input = new Input("myinput", $args); 117 | $input->toHTML(); 118 | } 119 | 120 | public function testBasicHTML() { 121 | $args = array('field_type' => 'meta_key', 'format' => 'html', 'values' => array('

Some HTML

')); 122 | $input = new Input("myinput", $args); 123 | $input->toHTML(); 124 | } 125 | 126 | public function testBasicHidden() { 127 | $args = array('field_type' => 'meta_key', 'format' => 'hidden', 'values' => array('myvalue')); 128 | $input = new Input("myinput", $args); 129 | $input->toHTML(); 130 | } 131 | 132 | } -------------------------------------------------------------------------------- /src/Form.php: -------------------------------------------------------------------------------- 1 | 'string', 19 | 'method' => 'RequestMethod', 20 | 'name' => 'string', 21 | 'class' => 'array', 22 | 'ajax' => 'object', 23 | 'disable_wrappers' => 'bool', 24 | 'auto_submit' => 'bool', 25 | 'inputs' => 'array' ); 26 | 27 | static protected $defaults = array( 28 | 'action' => '', 29 | 'method' => 'GET', 30 | 'name' => 'wp-advanced-search', 31 | 'class' => array('wp-advanced-search'), 32 | 'disable_wrappers' => false, 33 | 'auto_submit' => false, 34 | 'inputs' => array() ); 35 | 36 | function __construct($wpas_id, $args) { 37 | $this->wpas_id = $wpas_id; 38 | $args = $this->preProcessArgs($args); 39 | $args = $this->parseArgs($args, self::$defaults); 40 | $this->args = self::validate($args); 41 | 42 | foreach($this->args as $key => $value) { 43 | $this->$key = $value; 44 | } 45 | 46 | $this->id = "wp-advanced-search"; 47 | } 48 | 49 | /** 50 | * Returns the full HTML content of the form 51 | * 52 | * @return string 53 | */ 54 | public function toHTML() { 55 | global $post; 56 | 57 | $output = " 58 |
id."\" name=\"".$this->name."\" 59 | class=\"".$this->classString()."\" 60 | method=\"".$this->method."\" "; 61 | $output .= $this->dataAttributesString(); 62 | $output .= "action=\"".$this->action."\"> "; 63 | 64 | // URL fix if "pretty permalinks" are not enabled 65 | if ( get_option('permalink_structure') == '' && is_object($post) ) { 66 | $output .= ''; 67 | } 68 | 69 | foreach ($this->inputs as $input) { 70 | if ($input instanceof Input) { 71 | $output .= $input->toHTML(); 72 | } 73 | } 74 | 75 | if ($this->ajax->isEnabled()) { 76 | $output .= ""; 77 | } 78 | 79 | $output .= "wpas_id."\">"; 80 | $output .= ""; 81 | 82 | $output .= "
"; 83 | 84 | return $output; 85 | } 86 | 87 | /** 88 | * Add an Input object to the form 89 | * 90 | * @param Input $input 91 | */ 92 | public function addInput( Input $input ) { 93 | if ($this->disableWrappers()) $input->disableWrapper(); 94 | $this->inputs[] = $input; 95 | } 96 | 97 | /** 98 | * Process and return arguments 99 | * 100 | * @param $args 101 | * @return mixed 102 | */ 103 | private function preProcessArgs($args) { 104 | if (!empty($args['class']) && is_string($args['class'])) { 105 | $args['class'] = explode(' ', $args['class']); 106 | } 107 | if (empty($args['ajax'])) $args['ajax'] = new AjaxConfig(); 108 | return $args; 109 | } 110 | 111 | /** 112 | * Generates string of data attributes 113 | * 114 | * @return string 115 | */ 116 | private function dataAttributesString() { 117 | $output = ""; 118 | if ($this->ajax->isEnabled() == false) return $output; 119 | 120 | $output .= "data-ajax-button=\"".$this->ajax->buttonText()."\" "; 121 | $output .= "data-ajax-loading=\"".$this->ajax->loadingImage()."\" "; 122 | $show_default = ($this->ajax->showDefaultResults()) ? "1" : "0"; 123 | $output .= "data-ajax-show-default=\"".$show_default."\" "; 124 | $output .= "data-ajax-url-hash=\"".$this->ajax->urlHash()."\" "; 125 | 126 | return $output; 127 | } 128 | 129 | private function classString() { 130 | $str = implode(" ",$this->class); 131 | if ($this->ajax->isEnabled()) { 132 | $str .= " wpas-ajax-enabled"; 133 | } 134 | if ($this->auto_submit) { 135 | $str .= " wpas-autosubmit"; 136 | } 137 | return $str; 138 | } 139 | 140 | public function addClass($class) { 141 | if (!is_string($class)) return; 142 | $this->class[] = $class; 143 | } 144 | 145 | public function disableWrappers() { 146 | return ((defined('WPAS_DISABLE_WRAPPERS') && WPAS_DISABLE_WRAPPERS) || ($this->disable_wrappers)); 147 | } 148 | 149 | 150 | public function getInputs() { 151 | return $this->inputs; 152 | } 153 | 154 | public function getDefaults() { 155 | return self::$defaults; 156 | } 157 | 158 | public function getAttributes() { 159 | return array( 'id' => $this->id, 160 | 'class' => $this->class, 161 | 'method' => $this->method, 162 | 'action' => $this->action, 163 | 'name' => $this->name ); 164 | } 165 | 166 | } 167 | -------------------------------------------------------------------------------- /src/Type.php: -------------------------------------------------------------------------------- 1 | $v format 14 | // If $v is an array, $v corresponds to valid types which are 15 | // a subset of that type 16 | private static $types = array( 17 | "string" => true, 18 | "numeric" => true, 19 | "bool" => true, 20 | "scalar" => array("string", "numeric", "bool"), 21 | "array" => true, 22 | "object" => array("Input", "Form"), 23 | "RequestMethod" => true, 24 | "InputFormat" => true, 25 | "FieldType" => true, 26 | "RequestVar" => true, 27 | "Operator" => true, 28 | "Relation" => true, 29 | "Compare" => true, 30 | "DataType" => true 31 | ); 32 | 33 | // Validation functions for each type 34 | private static $validate = array( 35 | "string" => "is_string", 36 | "numeric" => "is_numeric", 37 | "bool" => "self::isBool", 38 | "scalar" => "is_scalar", 39 | "array" => "is_array", 40 | "object" => "is_object", 41 | "RequestMethod" => array("\WPAS\Enum\RequestMethod", "isValid"), 42 | "InputFormat" => array("\WPAS\Enum\InputFormat", "isValid"), 43 | "FieldType" => array("\WPAS\Enum\FieldType", "isValid"), 44 | "RequestVar" => array("\WPAS\Enum\RequestVar", "isValidValue"), 45 | 'Operator' => array("\WPAS\Enum\Operator", "isValidValue"), 46 | 'Compare' => array("\WPAS\Enum\Compare", "isValidValue"), 47 | 'Relation' => array("\WPAS\Enum\Relation", "isValidValue"), 48 | 'DataType' => array("\WPAS\Enum\DataType", "isValidValue") 49 | ); 50 | 51 | /** 52 | * Check if a string representing a type name is a valid type 53 | * 54 | * @param string $name 55 | * @return bool 56 | */ 57 | public static function isValidName($name) { 58 | if (!is_string($name)) return false; 59 | if (!empty($name) && isset(self::$types[$name])) { 60 | return true; 61 | } 62 | 63 | if ($subtype = self::isTypedArray($name)) { 64 | return self::isValidName($subtype); 65 | } 66 | 67 | return false; 68 | } 69 | 70 | /** 71 | * Check if a type name corresponds to an array type 72 | * 73 | * @param string $type 74 | * @return bool 75 | */ 76 | private static function isArray($type) { 77 | return (substr($type, 0, 5) == 'array'); 78 | } 79 | 80 | /** 81 | * Check if a type name corresponds to a typed array 82 | * 83 | * @param string $type 84 | * @return bool 85 | */ 86 | private static function isTypedArray($type) { 87 | $pattern = '/^array/'; 88 | $matches = array(); 89 | if ( !preg_match($pattern, $type, $matches) ) { 90 | return false; 91 | } 92 | return ( (count($matches) == 2) ? $matches[1] : false ); 93 | } 94 | 95 | /** 96 | * Return the basic type name of a given type string. 97 | * 98 | * eg: 99 | * getBasicType("array") returns "array" 100 | * getBasicType("bool") returns "bool" 101 | * 102 | * @param string $type 103 | * @return mixed 104 | */ 105 | private static function getBasicType($type) { 106 | if (!self::isValidName($type)) return false; 107 | return (self::isArray($type)) ? 'array' : $type; 108 | } 109 | 110 | /** 111 | * Check if a type name is considered a subtype of another type 112 | * 113 | * @param string $type 114 | * @param string $superset_type 115 | * @return bool 116 | */ 117 | public static function isSubtypeOf($type, $superset_type) { 118 | 119 | if (!self::isValidName($type) || !self::isValidName($superset_type)) { 120 | return false; 121 | } 122 | 123 | if ($type == $superset_type) return true; 124 | if (isset(self::$types[$superset_type]) && 125 | is_array(self::$types[$superset_type])) { 126 | $subtypes = self::$types[$superset_type]; 127 | if (in_array($type, $subtypes, true) === TRUE) { 128 | return true; 129 | } 130 | } 131 | 132 | if ( ($subtype = self::isTypedArray($type)) == false ) { 133 | return false; 134 | } 135 | 136 | if (!self::isArray($superset_type)) return false; 137 | 138 | 139 | if ( ($super_subtype = self::isTypedArray($superset_type)) == false ) { 140 | return true; 141 | } 142 | 143 | return self::isSubtypeOf($subtype, $super_subtype); 144 | } 145 | 146 | /** 147 | * Checks if a given value is of a specified type 148 | * 149 | * @param string $type 150 | * @param string $value 151 | * @return bool 152 | */ 153 | public static function matches($type, $value) { 154 | if (!self::isValidName($type)) return false; 155 | $basic = self::getBasicType($type); 156 | 157 | if (!call_user_func(self::$validate[$basic], $value)) { 158 | return false; 159 | } 160 | 161 | if ($subtype = self::isTypedArray($type)) { 162 | foreach($value as $v) { 163 | if (!call_user_func(self::$validate[$subtype], $v)) { 164 | return false; 165 | } 166 | } 167 | } 168 | 169 | return true; 170 | } 171 | 172 | /** 173 | * Flexible type checker for boolean values. Will accept strings 174 | * equal to 'true','false','0', or '1' in addition to real booleans 175 | * 176 | * @param $val 177 | * @return bool 178 | */ 179 | public static function isBool($val) { 180 | switch ($val) { 181 | case is_bool($val) : 182 | return true; 183 | case !is_string($val) : 184 | return false; 185 | case strtolower($val) == 'true' : 186 | case strtolower($val) == 'false' : 187 | case strtolower($val) == '1' : 188 | case strtolower($val) == '0' : 189 | return true; 190 | break; 191 | } 192 | return false; 193 | } 194 | 195 | 196 | } -------------------------------------------------------------------------------- /src/Field.php: -------------------------------------------------------------------------------- 1 | array('type' => 'FieldType', 'required' => true), 17 | 'inputs' => 'array', 18 | 'taxonomy' => 'string', 19 | 'meta_key' => 'string', 20 | 'data_type' => 'DataType', 21 | 'group_method' => array('type' => 'string', 'matches' => 'merge|distinct'), 22 | 'relation' => array('type'=>'Relation','required' => true), 23 | 'operator' => array('type'=>'Operator','required' => true), 24 | 'compare' => array('type'=>'Compare','required' => true), 25 | ); 26 | 27 | protected static $defaults = array( 28 | 'relation' => 'AND', 29 | 'group_method' => 'distinct', 30 | 'operator' => 'AND', 31 | 'compare' => '=', 32 | 'data_type' => DataType::_default 33 | ); 34 | 35 | private static $input_args = array( 36 | 'format' => 1, 37 | 'values' => 1, 38 | 'value' => 1, 39 | 'default' => 1, 40 | 'default_all' => 1, 41 | 'field_type' => 1, 42 | 'nested' => 1, 43 | 'placeholder' => 1, 44 | 'class' => 1, 45 | 'attributes' => 1, 46 | 'allow_null' => 1, 47 | 'exclude' => 1, 48 | 'compare' => 1, 49 | 'operator' => 1, 50 | 'pre_html' => 1, 51 | 'post_html' => 1, 52 | 'label' => 1, 53 | 'taxonomy' => 1, 54 | 'terms' => 1, 55 | 'authors' => 1, 56 | 'relation' => 1, 57 | 'data_type' => 1, 58 | 'date_type' => 1, 59 | 'orderby_values' => 1, 60 | 'id' => 1, 61 | 'term_args' => 1 62 | ); 63 | 64 | public function __construct($args) { 65 | $args = $this->parseArgs($args,self::$defaults); 66 | $args = self::validate($args); 67 | $args = $this->postProcessArgs($args); 68 | $this->inputs = array(); 69 | $this->field_id = $this->setFieldId($args); 70 | $this->field_type = $args['field_type']; 71 | $this->relation = $args['relation']; 72 | $this->group_method = $args['group_method']; 73 | $this->operator = $args['operator']; 74 | $this->compare = $args['compare']; 75 | $this->data_type = $args['data_type']; 76 | $this->populateInputs($this->field_id, $args); 77 | } 78 | 79 | private function populateInputs($field_id, $args) { 80 | if (!empty($args['inputs'])) { 81 | $count = count($args['inputs']); 82 | if ($count == 1) { 83 | $input = $this->addRemainingArgs(reset($args['inputs']), $args); 84 | $this->inputs[$field_id] = $input; 85 | } else { 86 | for($i=1; $i<=$count; $i++) { 87 | $input = $this->addRemainingArgs($args['inputs'][$i-1], $args); 88 | $this->inputs[$field_id.$i] = $input; 89 | } 90 | } 91 | } else { 92 | $this->inputs[$field_id] = array_intersect_key($args, 93 | self::$input_args); 94 | } 95 | } 96 | 97 | private function addRemainingArgs($input_args, $field_args) { 98 | if (isset($field_args['inputs'])) unset($field_args['inputs']); 99 | $input_args = parent::parseArgs($input_args, $field_args); 100 | 101 | if ($inner_type = DataType::isArrayType($input_args['data_type'])) { 102 | $input_args['data_type'] = $inner_type; 103 | } 104 | 105 | return $input_args; 106 | } 107 | 108 | private function setFieldId($args) { 109 | switch($args['field_type']) { 110 | case 'meta_key' : 111 | if (empty($args['meta_key'])) { 112 | throw new \Exception('Field is missing '. 113 | 'argument \'meta_key\''); 114 | return; 115 | } 116 | return $args['meta_key']; 117 | case 'taxonomy' : 118 | if (empty($args['taxonomy'])) { 119 | throw new \Exception('Field is missing '. 120 | 'argument \'taxonomy\''); 121 | return; 122 | } 123 | return $args['taxonomy']; 124 | default: 125 | return $args['field_type']; 126 | } 127 | } 128 | 129 | protected static function parseArgs(array $args, array $defaults) { 130 | $args = parent::parseArgs($args, self::$defaults); 131 | return $args; 132 | } 133 | 134 | private function postProcessArgs(array $args) { 135 | $args['field_type'] = $args['type']; 136 | unset($args['type']); 137 | return $args; 138 | } 139 | 140 | /** 141 | * @return mixed 142 | */ 143 | public function getFieldType() 144 | { 145 | return $this->field_type; 146 | } 147 | 148 | /** 149 | * @return mixed 150 | */ 151 | public function getRelation() 152 | { 153 | return $this->relation; 154 | } 155 | 156 | /** 157 | * @return array 158 | */ 159 | public function getInputs() 160 | { 161 | return $this->inputs; 162 | } 163 | 164 | /** 165 | * @return array 166 | */ 167 | public static function getDefaults() 168 | { 169 | return self::$defaults; 170 | } 171 | 172 | /** 173 | * @return void 174 | */ 175 | public function getFieldId() 176 | { 177 | return $this->field_id; 178 | } 179 | 180 | /** 181 | * @return mixed 182 | */ 183 | public function getGroupMethod() 184 | { 185 | return $this->group_method; 186 | } 187 | 188 | /** 189 | * @return mixed 190 | */ 191 | public function getCompare() 192 | { 193 | return $this->compare; 194 | } 195 | 196 | /** 197 | * @return mixed 198 | */ 199 | public function getOperator() 200 | { 201 | return $this->operator; 202 | } 203 | 204 | /** 205 | * @return mixed 206 | */ 207 | public function getDataType() 208 | { 209 | return $this->data_type; 210 | } 211 | 212 | } -------------------------------------------------------------------------------- /src/Input.php: -------------------------------------------------------------------------------- 1 | 'string', 24 | 'attributes' => 'array', 25 | 'field_type' => array('type' => 'FieldType', 26 | 'required' => true), 27 | 'label' => 'string', 28 | 'class' => 'array', 29 | 'format' => array('type' => 'InputFormat', 30 | 'required' => true), 31 | 'placeholder' => 'string|bool', 32 | 'values' => 'array', 33 | 'selected' => 'array', 34 | 'nested' => 'bool', 35 | 'allow_null' => 'bool|string', 36 | 'default_all' => 'bool', 37 | 'pre_html' => 'string', 38 | 'post_html' => 'string'); 39 | 40 | protected static $defaults = array( 41 | 'label' => '', 42 | 'placeholder' => false, 43 | 'values' => array(), 44 | 'selected' => array(), 45 | 'nested' => false, 46 | 'allow_null' => false, 47 | 'default_all' => false, 48 | 'disable_wrapper' => false, 49 | 'pre_html' => '', 50 | 'post_html' => '' ); 51 | 52 | public function __construct($input_name, $args = array()) { 53 | $args = $this->parseArgs($args,self::$defaults); 54 | $args = $this->validateInput($input_name, $args, self::$defaults); 55 | $this->initMembers($input_name, $args); 56 | } 57 | 58 | /** 59 | * Validates the input_name and arguments 60 | * 61 | * @param string $input_name 62 | * @param array $args 63 | * @throws \Exception 64 | * @throws \InvalidArgumentException 65 | * @return array 66 | */ 67 | private function validateInput( $input_name, $args, $defaults ) { 68 | $args = self::validate($args); 69 | 70 | if (!is_string($input_name)) { 71 | $err_msg = $this->validationErrorMsg( 72 | array('Argument 1 `$field_name` ' . 73 | 'must be a string.')); 74 | throw new \InvalidArgumentException($err_msg); 75 | } 76 | 77 | return $args; 78 | } 79 | 80 | /** 81 | * Initializes object members 82 | * 83 | * @param string $input_name 84 | * @param array $args 85 | */ 86 | private function initMembers($input_name, $args) { 87 | $this->input_name = $input_name; 88 | 89 | foreach($args as $key => $value) { 90 | $this->$key = $value; 91 | } 92 | 93 | // For select fields, add null value if specified 94 | if ($this->format == 'select' && $this->allow_null && !empty($this->values)) { 95 | $null_val = ($this->allow_null === true) ? '' : $this->allow_null; 96 | $this->addNullOption($null_val); 97 | } 98 | 99 | if (!empty($this->class) && is_array($this->class)) { 100 | $this->class = implode(' ', $this->class); 101 | } 102 | 103 | $this->id = $this->input_name; 104 | $this->ctr = 1; 105 | } 106 | 107 | /** 108 | * Returns a string containing the full HTML content of the input, including 109 | * a wrapper div 110 | * 111 | * @return string 112 | */ 113 | public function toHTML() 114 | { 115 | $markup = new InputMarkup($this); 116 | return $markup->generate(); 117 | } 118 | 119 | /** 120 | * For select fields, adds a null option to the beginning of the menu 121 | * 122 | * @since 1.3 123 | */ 124 | private function addNullOption( $null_label ) { 125 | if ($this->nested) { 126 | $null_option = array( 127 | 'value' => '', 128 | 'label' => $null_label, 129 | 'children' => array() 130 | ); 131 | } else { 132 | $null_option = $null_label; 133 | } 134 | 135 | $arr = array_reverse($this->values, true); 136 | $arr[''] = $null_option; 137 | $arr = array_reverse($arr, true); 138 | $this->values = $arr; 139 | } 140 | 141 | public function disableWrapper() { 142 | $this->disable_wrapper = true; 143 | } 144 | 145 | public function wrappersDisabled() { 146 | return $this->disable_wrapper; 147 | } 148 | 149 | public function getId() 150 | { 151 | return $this->id; 152 | } 153 | 154 | public function getFormat() 155 | { 156 | return $this->format; 157 | } 158 | 159 | public function getInputName() 160 | { 161 | return $this->input_name; 162 | } 163 | 164 | public function getClass() 165 | { 166 | return $this->class; 167 | } 168 | 169 | public function getFieldType() { 170 | return $this->field_type; 171 | } 172 | 173 | public function getAttributes() 174 | { 175 | return $this->attributes; 176 | } 177 | 178 | public function getLabel() 179 | { 180 | return $this->label; 181 | } 182 | 183 | public function getPlaceholder() 184 | { 185 | return $this->placeholder; 186 | } 187 | 188 | public function getValues() 189 | { 190 | return $this->values; 191 | } 192 | 193 | public function isNested() 194 | { 195 | return $this->nested; 196 | } 197 | 198 | public function getSelected() 199 | { 200 | return $this->selected; 201 | } 202 | 203 | public function getPreHtml() 204 | { 205 | return $this->pre_html; 206 | } 207 | 208 | public function getPostHtml() 209 | { 210 | return $this->post_html; 211 | } 212 | 213 | } -------------------------------------------------------------------------------- /src/Query.php: -------------------------------------------------------------------------------- 1 | 'toScalar', 16 | 'posts_per_page' => 'toScalar', 17 | 'order' => 'toScalar' 18 | ); 19 | 20 | private static function filter($query_var, $value) { 21 | if (empty(self::$filters[$query_var])) return $value; 22 | $func = self::$filters[$query_var]; 23 | return call_user_func('self::'.$func,$value); 24 | } 25 | 26 | private static function toScalar($value) { 27 | if (is_array($value)) return reset($value); 28 | return $value; 29 | } 30 | 31 | public function __construct(array $fields_table, array $wp_query_args, HttpRequest $request) { 32 | $this->wp_query_args = $wp_query_args; 33 | $this->fields = $fields_table; 34 | $this->request = $request; 35 | $this->orderby_meta_keys = $this->initOrderBy(); 36 | $this->query = $this->build(); 37 | } 38 | 39 | /** 40 | * Populate orderby_meta_keys table 41 | * 42 | * Keeps track of which orderby options (if any) are meta_keys 43 | * and whether they are character-based or numeric values 44 | */ 45 | private function initOrderBy() { 46 | $orderby_meta_keys = array(); 47 | $field_group = $this->fields[FieldType::orderby]; 48 | $orderby_fields = $field_group->getFields(); 49 | 50 | if (empty($orderby_fields)) return $orderby_meta_keys; 51 | 52 | 53 | $field = $orderby_fields[0]; 54 | $inputs = $field->getInputs(); 55 | 56 | if (empty($inputs[FieldType::orderby]) || empty($inputs[FieldType::orderby]['orderby_values'])) { 57 | return $orderby_meta_keys; 58 | } 59 | 60 | $values = $inputs[FieldType::orderby]['orderby_values']; 61 | 62 | foreach ($values as $k=>$v) { 63 | if (isset($v['meta_key']) && $v['meta_key']) { 64 | if (isset($v['orderby']) && $v['orderby'] == 'meta_value_num') { 65 | $type = $v['orderby']; 66 | } else { 67 | $type = 'meta_value'; 68 | } 69 | $orderby_meta_keys[$k] = $type; 70 | } 71 | } 72 | 73 | return $orderby_meta_keys; 74 | } 75 | 76 | 77 | /** 78 | * Assembles an array of WP_Query arguments 79 | * 80 | * @return array 81 | */ 82 | private function build() { 83 | $query = array(); 84 | $skip = array('meta_key'=> 1, 'taxonomy'=> 1, 'date'=> 1, 'orderby'=> 1); 85 | 86 | 87 | foreach ($this->fields as $type => $field_group) { 88 | $fields = $field_group->getFields(); 89 | if (empty($fields) || isset($skip[$type])) continue; 90 | $query = $this->addQueryArg($query, $fields, $this->request); 91 | } 92 | 93 | $query = $this->addOrderbyArg($query, $this->request); 94 | $query = $this->addSubQuery($query, $this->fields['taxonomy'], 'taxonomy', $this->request); 95 | $query = $this->addSubQuery($query, $this->fields['meta_key'], 'meta_key', $this->request); 96 | $query = $this->addSubQuery($query, $this->fields['date'], 'date', $this->request); 97 | $query = $this->addPaginationArg($query); 98 | 99 | return self::parseArgs($query, $this->wp_query_args); 100 | } 101 | 102 | /** 103 | * Takes an array of query arguments and adds an orderby argument 104 | * 105 | * @param array $query 106 | * @param HttpRequest $request 107 | * @return array 108 | */ 109 | private function addOrderbyArg(array $query, HttpRequest $request) { 110 | $var = RequestVar::orderby; 111 | $val = $request->get($var); 112 | 113 | if (empty($val)) return $query; 114 | $orderby_val = $val; 115 | $orderby_val = (is_array($orderby_val)) ? implode(" ",$orderby_val) : $orderby_val; 116 | 117 | if (array_key_exists($orderby_val, $this->orderby_meta_keys)) { 118 | $query[$var] = $this->orderby_meta_keys[$orderby_val]; 119 | $query['meta_key'] = $orderby_val; 120 | } else { 121 | $query[$var] = $orderby_val; 122 | } 123 | 124 | return $query; 125 | } 126 | 127 | /** 128 | * Adds and argument to an array of query arguments 129 | * 130 | * @param array $query 131 | * @param array $fields 132 | * @param HttpRequest $request 133 | * @return array 134 | */ 135 | private function addQueryArg(array $query, array $fields, HttpRequest $request) { 136 | if (empty($fields)) return $query; 137 | $field = reset($fields); // As of v1.4, only one field allowed per 138 | // query var (other than taxonomy and meta_key) 139 | $field_id = $field->getFieldId(); 140 | 141 | $var = RequestVar::nameToVar($field_id); 142 | 143 | $wp_var = RequestVar::wpQueryVar($field_id); 144 | $wp_var = (!$wp_var) ? $var : $wp_var; 145 | 146 | $val = $request->get($var); 147 | 148 | if (empty($val)) return $query; 149 | 150 | $query[$wp_var] = self::filter($wp_var,$val); 151 | return $query; 152 | } 153 | 154 | /** 155 | * Takes an array of query arguments and adds a sub-query 156 | * (eg tax_query, meta_query, or date_query) 157 | * 158 | * @param array $query 159 | * @param FieldGroup $field_group 160 | * @param $field_type 161 | * @param HttpRequest $request 162 | * @return array 163 | */ 164 | private function addSubQuery(array $query, FieldGroup $field_group, $field_type, HttpRequest $request) { 165 | $classnames = array('taxonomy' => 'TaxQuery', 'meta_key' => 'MetaQuery', 'date' => 'DateQuery'); 166 | $fields = $field_group->getFields(); 167 | if (empty($classnames[$field_type]) || empty($fields)) return $query; 168 | 169 | $s_query = $this->getSubQuery($classnames[$field_type], $fields, $field_group->getRelation(), $request ); 170 | 171 | if (!empty($s_query)) $query[RequestVar::wpQueryVar($field_type)] = $s_query; 172 | return $query; 173 | } 174 | 175 | /** 176 | * Creates and returns a sub-query 177 | * (eg tax_query, meta_query, or date_query) 178 | * 179 | * @param $class 180 | * @param $fields 181 | * @param $relation 182 | * @param HttpRequest $request 183 | * @return mixed 184 | */ 185 | private function getSubQuery($class, $fields, $relation, HttpRequest $request) { 186 | $class = 'WPAS\\'.$class; 187 | if ($class == 'WPAS\DateQuery') { 188 | $query = new $class($fields[0], $request); // Allow only 1 field for DateQuery 189 | } else { 190 | $query = new $class($fields, $relation, $request); 191 | } 192 | return $query->getQuery(); 193 | } 194 | 195 | /** 196 | * Adds pagination argument to an array of query arguments 197 | * 198 | * @param array $query 199 | * @return array 200 | */ 201 | private function addPaginationArg(array $query) { 202 | $page_num = $this->request->get('paged'); 203 | if (!empty($page_num)) { 204 | $paged = $page_num; 205 | } 206 | else if ( get_query_var('paged') ) { 207 | $paged = get_query_var('paged'); 208 | } else if ( get_query_var('page') ) { 209 | $paged = get_query_var('page'); 210 | } else { 211 | $paged = 1; 212 | } 213 | $query['paged' ] = $paged; 214 | return $query; 215 | } 216 | 217 | public function getArgs() { 218 | return $this->query; 219 | } 220 | 221 | } -------------------------------------------------------------------------------- /wpas.php: -------------------------------------------------------------------------------- 1 | errors = array(); 29 | $this->request = array(); 30 | $this->args = $this->get_form_args($id); 31 | $this->args = $this->process_args($this->args); 32 | $this->ajax = $this->args['form']['ajax']; 33 | $this->debug = $this->args['debug']; 34 | $this->debug_level = $this->args['debug_level']; 35 | $this->factory = new WPAS\Factory($this->args, $request); 36 | $this->query = $this->factory->buildQueryObject(); 37 | } 38 | 39 | /** 40 | * Get arguments for a form based on its registered ID 41 | * 42 | * @param $id 43 | * @return array|mixed 44 | */ 45 | private function get_form_args($id) { 46 | global $WPAS_FORMS; 47 | 48 | if (empty($WPAS_FORMS)) { 49 | $this->errors[] = "No forms have been configured."; 50 | return array(); 51 | } 52 | if (empty($id)) { 53 | if (!empty($WPAS_FORMS['default'])) return $WPAS_FORMS['default']; 54 | else return reset($WPAS_FORMS); 55 | } else if (empty($WPAS_FORMS[$id])) { 56 | $this->errors[] = "WPAS form with ID \"".$id."\" is not registered."; 57 | return array(); 58 | } 59 | return $WPAS_FORMS[$id]; 60 | } 61 | 62 | /** 63 | * Print HTML content of the search form 64 | */ 65 | public function the_form() { 66 | $form = $this->factory->getForm(); 67 | if ($this->debug) $form->addClass('wpas-debug-enabled'); 68 | echo $form->toHTML(); 69 | } 70 | 71 | /** 72 | * Create and return WP_Query object for the search instance 73 | * 74 | * @return WP_Query 75 | */ 76 | public function query() { 77 | if (!$this->ajax_enabled()) $this->print_debug(); 78 | return $this->query; 79 | } 80 | 81 | /** 82 | * Displays range of results displayed on the current page. 83 | * 84 | * @return string 85 | */ 86 | function results_range( $args = array() ) { 87 | return WPAS\Helper\ResultsRange::make($this->query, $args); 88 | } 89 | 90 | /** 91 | * Displays pagination links 92 | */ 93 | public function pagination( $args = array() ) { 94 | echo WPAS\Helper\Pagination::make($this->query, $args, $this->ajax_enabled()); 95 | } 96 | 97 | /** 98 | * Create string of debug information 99 | * 100 | * For use when WPAS_DEBUG is enabled, or when calling the 101 | * print_debug() method. 102 | * 103 | * When $log is set to 'verbose', the output will contain a full var dump 104 | * of the generated WP_Query object. 105 | * 106 | * @param string $level 107 | * @return string 108 | */ 109 | public function create_debug_output() { 110 | $level = $this->debug_level; 111 | $errors = $this->get_errors(); 112 | $wp_query_obj = $this->factory->getWPQuery(); 113 | 114 | 115 | $output = "WPAS DEBUG\n"; 116 | 117 | $output .= "------------------------------------\n"; 118 | $output .= "|| Errors\n"; 119 | $output .= "------------------------------------\n"; 120 | 121 | if (empty($errors)) { 122 | $output .= "No errors detected.\n"; 123 | } else { 124 | $output .= count($errors) . " errors detected:\n"; 125 | $output .= print_r($errors, true) . "\n"; 126 | } 127 | 128 | $output .= "------------------------------------\n"; 129 | $output .= "|| WP_Query Arguments\n"; 130 | $output .= "------------------------------------\n"; 131 | 132 | $output .= print_r($wp_query_obj->query, true) . "\n"; 133 | 134 | $output .= "------------------------------------\n"; 135 | $output .= "|| MySQL Query \n"; 136 | $output .= "------------------------------------\n"; 137 | 138 | $output .= print_r($wp_query_obj->request, true) . "\n"; 139 | 140 | $output .= "------------------------------------\n"; 141 | $output .= "|| Request Data \n"; 142 | $output .= "------------------------------------\n"; 143 | 144 | $output .= print_r($this->get_request(), true) . "\n"; 145 | 146 | if ($level == 'verbose') { 147 | $output .= "------------------------------------\n"; 148 | $output .= "|| WP_Query Object Dump\n"; 149 | $output .= "------------------------------------\n"; 150 | $output .= print_r($wp_query_obj, true); 151 | } 152 | 153 | return $output; 154 | } 155 | 156 | /** 157 | * Print debug information 158 | */ 159 | public function print_debug() { 160 | if ($this->debug === false) return; 161 | $output = $this->create_debug_output(); 162 | echo '
' . $output . '
'; 163 | } 164 | 165 | /** 166 | * @return bool 167 | */ 168 | public function debug_enabled() { 169 | return $this->debug; 170 | } 171 | 172 | /** 173 | * Get array of errors generated during setup/configuration of search 174 | * instance 175 | * 176 | * @return array 177 | */ 178 | public function get_errors() { 179 | $errors = $this->errors; 180 | if (is_object($this->factory)) { 181 | $errors = array_merge($this->errors, $this->factory->getErrors()); 182 | } 183 | return $errors; 184 | } 185 | 186 | public function set_error($err_msg) { 187 | $this->errors[] = $err_msg; 188 | } 189 | 190 | /** 191 | * Get Ajax configuration 192 | * 193 | * @return mixed 194 | */ 195 | public function get_ajax() { 196 | return $this->ajax; 197 | } 198 | 199 | /** 200 | * Returns true if ajax is enabled for the current search instance 201 | * 202 | * @return bool 203 | */ 204 | public function ajax_enabled() { 205 | return $this->ajax->isEnabled(); 206 | } 207 | 208 | /** 209 | * Pre process arguments, translate argument blocks into config objects 210 | * 211 | * @param $args 212 | * @return mixed 213 | */ 214 | private function process_args($args) { 215 | // Establish AJAX configuration 216 | $args = $this->set_ajax_config($args); 217 | 218 | // Set debug mode and debug level 219 | $args = $this->set_debug_args($args); 220 | 221 | return $args; 222 | } 223 | 224 | private function set_ajax_config($args) { 225 | $ajax_args = array(); 226 | 227 | if (!isset($args['form'])) $args['form'] = array(); 228 | 229 | if (isset($args['form']['ajax'])) { 230 | $ajax_args = $args['form']['ajax']; 231 | } 232 | $args['form']['ajax'] = new WPAS\AjaxConfig($ajax_args); 233 | 234 | return $args; 235 | } 236 | 237 | private function set_debug_args($args) 238 | { 239 | $debug = false; 240 | if (defined('WPAS_DEBUG') && WPAS_DEBUG) { 241 | $debug = true; 242 | } else if (!empty($args['debug']) && $args['debug']) { 243 | $debug = true; 244 | } 245 | 246 | $debug_level = 'log'; 247 | 248 | if (defined('WPAS_DEBUG_LEVEL') && WPAS_DEBUG_LEVEL) { 249 | $debug_level = WPAS_DEBUG_LEVEL; 250 | } else if (!empty($args['debug_level']) && $args['debug_level']) { 251 | $debug_level = $args['debug_level']; 252 | } 253 | 254 | $args['debug'] = $debug; 255 | $args['debug_level'] = $debug_level; 256 | 257 | return $args; 258 | } 259 | 260 | private function get_request() { 261 | $request = $this->factory->getRequest(); 262 | return $request->all(); 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/MetaQuery.php: -------------------------------------------------------------------------------- 1 | query = $this->build($fields, $relation, $request); 14 | } 15 | 16 | /** 17 | * Build and return a meta_query argument array 18 | * 19 | * @param array $fields 20 | * @param string $relation 21 | * @param $request 22 | * @return array 23 | */ 24 | public function build(array $fields, $relation = 'AND', HttpRequest $request) { 25 | $query = array(); 26 | $this->request = $request; 27 | 28 | if (empty($fields)) return $query; 29 | foreach ($fields as $field) { 30 | $group = $this->metaQueryGroup($field, $request); 31 | if (!empty($group)) $query[] = $this->metaQueryGroup($field, $request); 32 | } 33 | 34 | if (count($query) > 1) $query['relation'] = $relation; 35 | 36 | return $query; 37 | } 38 | 39 | /** 40 | * Build a meta_query group comprising query arguments and values related 41 | * to a single meta_key 42 | * 43 | * @param $field 44 | * @param $request 45 | * @return array 46 | */ 47 | private function metaQueryGroup($field, HttpRequest $request) { 48 | $group = array(); 49 | $meta_key = $field->getFieldId(); 50 | $inputs = $field->getInputs(); 51 | $data_type = $field->getDataType(); 52 | $compare = $field->getCompare(); 53 | 54 | $clauses = $this->clauseList($inputs, $meta_key, $compare, $data_type); 55 | 56 | foreach ($clauses as $clause) { 57 | if (!empty($clause)) { 58 | $group[] = $clause; 59 | } 60 | } 61 | if (count($group) == 1) $group = $group[0]; 62 | return $group; 63 | } 64 | 65 | /** 66 | * Construct array of meta_query clauses 67 | * 68 | * @param $inputs 69 | * @param $meta_key 70 | * @param $compare 71 | * @param $data_type 72 | * @return array 73 | */ 74 | private function clauseList($inputs, $meta_key, $compare, $data_type) { 75 | if ($compare == Compare::between) { 76 | return array($this->metaQueryClauseBetween($meta_key, $inputs, 2)); 77 | } 78 | 79 | $clauses = array(); 80 | 81 | // Disallow multiple input sources if not using BETWEEN comparison 82 | // This is a (potentially) temporary restriction 83 | $keys = array_keys($inputs); 84 | $inputs = array($keys[0] => $inputs[$keys[0]]); 85 | // 86 | // 87 | 88 | foreach ($inputs as $input_name => $input) { 89 | if (DataType::isArrayType($data_type)) { 90 | $clause = $this->metaQueryClauseArray($meta_key, $input_name, $input); 91 | } else { 92 | $clause = $this->metaQueryClause($meta_key, $input_name, $input); 93 | } 94 | $clauses[] = $clause; 95 | } 96 | 97 | return $clauses; 98 | 99 | } 100 | 101 | /** 102 | * Build a meta_query clause corresponding to a single input 103 | * 104 | * @param $meta_key 105 | * @param $input_name 106 | * @param $input 107 | * @return array 108 | */ 109 | private function metaQueryClause($meta_key, $input_name, $input) { 110 | if (empty($input)) return array(); 111 | 112 | $request_var = RequestVar::nameToVar($input_name, 'meta_key'); 113 | $val = $this->request->get($request_var); 114 | if (empty($val)) return array(); 115 | 116 | $clause = array(); 117 | $clause['key'] = $meta_key; 118 | $clause['compare'] = $input['compare']; 119 | $clause['value'] = $val; 120 | $clause['type'] = $input['data_type']; 121 | 122 | return $clause; 123 | } 124 | 125 | /** 126 | * Build a meta_query clause for a BETWEEN relationship 127 | * 128 | * @param $meta_key 129 | * @param array $inputs 130 | * @param bool $limit 131 | * @return array 132 | */ 133 | private function metaQueryClauseBetween($meta_key, array $inputs, $limit = false) { 134 | if (empty($inputs)) return array(); 135 | $first_input = reset($inputs); 136 | $clause = array(); 137 | 138 | $clause['key'] = $meta_key; 139 | $clause['type'] = $first_input['data_type']; 140 | $clause['value'] = array(); 141 | $clause['compare'] = Compare::between; 142 | $count = 1; 143 | 144 | foreach($inputs as $name => $input) { 145 | $clause['value'] = $this->mergeRequestValues($clause['value'], $name); 146 | $count++; 147 | if ($limit && $count > $limit) break; 148 | } 149 | 150 | if (!empty($compare)) { 151 | $clause['compare'] = $compare; 152 | } else if (count($inputs) == 1) { 153 | $clause['compare'] = $first_input['compare']; 154 | 155 | // Support single-input BETWEEN fields using range values, i.e. ['0-10','11-25'] 156 | if (count($clause['value']) == 1) { 157 | $clause = $this->adaptClauseForSingleInput($clause); 158 | } 159 | } 160 | 161 | if ( empty($clause['value']) ) return ''; 162 | if ( empty($clause['value'][0]) && !is_numeric($clause['value'][0])) return ''; 163 | 164 | return $clause; 165 | } 166 | 167 | /** 168 | * Appends a value from the HTTP request an array of values 169 | * Used for constructing multi-input BETWEEN comparisons 170 | * 171 | * @param array $values Existing values array 172 | * @param $name Request var name 173 | * @return array 174 | */ 175 | private function mergeRequestValues(array $values, $name) { 176 | $name = RequestVar::nameToVar($name, 'meta_key'); 177 | $val = $this->request->get($name, null); 178 | 179 | if ($val === null) { 180 | return $values; 181 | } 182 | 183 | // Disallow multi-value inputs 184 | // Reason: Doing a BETWEEN comparison between two multi-value 185 | // groups is undefined behavior 186 | $val = (is_array($val) ) ? array(reset($val)) : array($val); 187 | 188 | return array_merge($values, $val); 189 | } 190 | 191 | /** 192 | * Build a meta_query clause for meta values which are stored as an array 193 | * 194 | * Creates a separate sub-clause for each value being queried 195 | * 196 | * @param $meta_key 197 | * @param $input_name 198 | * @param $input 199 | * @return array 200 | */ 201 | private function metaQueryClauseArray($meta_key, $input_name, $input) { 202 | $request_var = RequestVar::nameToVar($input_name, 'meta_key'); 203 | $var = $this->request->get($request_var); 204 | if (empty($var)) return array(); 205 | 206 | $clause = array(); 207 | 208 | if (!is_array($var)) $var = array($var); 209 | 210 | foreach($var as $value) { 211 | $clause[] = $this->subClause($meta_key, $input, $value); 212 | } 213 | 214 | if (count($clause) > 1) { 215 | $clause['relation'] = $input['relation']; 216 | } 217 | 218 | if (count($clause) == 1) $clause = $clause[0]; 219 | 220 | return $clause; 221 | } 222 | 223 | private function subClause($meta_key, $input, $value) { 224 | return array( 225 | 'key' => $meta_key, 226 | 'type' => DataType::isArrayType($input['data_type']), 227 | 'value' => $value, 228 | 'compare' => $input['compare'] 229 | ); 230 | } 231 | 232 | /** 233 | * Adapts meta_query clause for single-input fields using range values 234 | * of the form ['0:10','11:24','25:'] 235 | * 236 | * @param $clause 237 | * @return array 238 | */ 239 | private function adaptClauseForSingleInput($clause) { 240 | if (empty($clause['value'])) return $clause; 241 | if (substr($clause['value'][0],-1) == ':') { 242 | $clause['value'] = substr($clause['value'][0],0,-1); 243 | $clause['compare'] = Compare::greq; 244 | return $clause; 245 | } 246 | if (substr($clause['value'][0],0,1) == ':') { 247 | $clause['value'] = substr($clause['value'][0],1); 248 | $clause['compare'] = Compare::leq; 249 | return $clause; 250 | } 251 | $clause['value'] = explode(":", $clause['value'][0]); 252 | return $clause; 253 | } 254 | 255 | /** 256 | * @return mixed 257 | */ 258 | public function getQuery() 259 | { 260 | return $this->query; 261 | } 262 | 263 | } -------------------------------------------------------------------------------- /src/Validator.php: -------------------------------------------------------------------------------- 1 | errors = array(); 24 | $this->passed = false; 25 | $this->args = $args; 26 | $this->invalid_args = array(); 27 | 28 | if (empty($rules)) { // If no rules provided, validation must pass 29 | $this->passed = true; 30 | return; 31 | } 32 | 33 | foreach ($rules as $arg => $rule) { 34 | if ($this->validateRule($arg,$rule) == false) { 35 | $this->invalid_args[$arg] = $arg; 36 | } 37 | } 38 | 39 | if (empty($this->invalid_args)) { 40 | $this->passed = true; 41 | } 42 | 43 | if (!$this->passed && $this->canOverride($defaults)) { 44 | $this->args = $this->defaultOverride($this->args, $defaults,$this->invalid_args); 45 | $this->passed = true; 46 | } 47 | } 48 | 49 | /** 50 | * Validates an argument against a rule string 51 | * 52 | * @param string $arg 53 | * @param string $rule 54 | * @return bool 55 | */ 56 | public function validateRule($arg, $rule) { 57 | $required = $this->isRequired($rule); 58 | $types = $this->getTypeString($rule); 59 | $matches = $this->getMatchesString($rule); 60 | 61 | if (!empty($this->args[$arg])) { 62 | return ($this->validateTypes($this->args[$arg], $arg, $types) && 63 | $this->validateMatches($this->args[$arg], $arg, $matches) ); 64 | } else if ($required) { 65 | $this->errors[] = "Argument '".$arg."' required but not provided."; 66 | return false; 67 | } 68 | return true; 69 | } 70 | 71 | /** 72 | * Extract and return the type string from a rule 73 | * 74 | * @param string|array $rule 75 | * @return mixed 76 | */ 77 | public function getTypeString($rule) { 78 | if (is_array($rule)) { 79 | return (empty($rule['type'])) ? false : $rule['type']; 80 | } 81 | if ($rule == 'required') { 82 | return false; 83 | } 84 | return (is_string($rule)) ? $rule : false; 85 | } 86 | 87 | /** 88 | * Determines if a given argument rule is specified as required. 89 | * 90 | * @param string|array $rule 91 | * @return bool 92 | */ 93 | public function isRequired($rule) { 94 | if (is_array($rule)) { 95 | if (!empty($rule['required'])) { 96 | return $rule['required']; 97 | } 98 | return false; 99 | } 100 | return ($rule == 'required'); 101 | } 102 | 103 | /** 104 | * Returns a 'matches' string from a rule if present. False 105 | * otherwise. 106 | * 107 | * @param string|array $rule 108 | * @return mixed 109 | */ 110 | public function getMatchesString($rule) { 111 | if (!is_array($rule) || empty($rule['matches'])) { 112 | return false; 113 | } 114 | if (!is_string($rule['matches'])) return false; 115 | return strtolower($rule['matches']); 116 | } 117 | 118 | /** 119 | * Validates a value against a formatted string of allowed types. 120 | * 121 | * Returns true if $value matches at least one allowed type, 122 | * false otherwise. 123 | * 124 | * @param mixed value 125 | * @param string $arg 126 | * @param string $types 127 | * @return bool 128 | */ 129 | public function validateTypes($value, $arg, $types) { 130 | if (!$types) return true;// False value indicates no rule was 131 | // provided, so validation passes by default 132 | $types_r = $this->parseRuleString($types); 133 | 134 | foreach($types_r as $t) { 135 | if ($this->validateType($value, $t) == true) return true; 136 | } 137 | $this->addTypeError($arg, $types, gettype($value), $value); 138 | return false; 139 | } 140 | 141 | public function validateMatches($value, $arg, $matches) { 142 | if (!$matches) return true; // False value indicates no rule was 143 | // provided, so validation passes by default 144 | $matches_r = $this->parseRuleString($matches); 145 | $value = (is_string($value)) ? strtolower($value) : $value; 146 | foreach($matches_r as $m) { 147 | if ($m == $value) return true; 148 | } 149 | return false; 150 | } 151 | 152 | /** 153 | * Validates a value against a type name. 154 | * 155 | * Returns true if $value is of type $type, 156 | * false otherwise. 157 | * 158 | * @param mixed $value 159 | * @param string $type 160 | * @return bool 161 | */ 162 | public static function validateType($value, $type) { 163 | if (!Type::isValidName($type)) { 164 | $msg = sprintf("Misformatted type string. '%s' is 165 | not a valid type name.", $type); 166 | throw new \InvalidArgumentException($msg); 167 | return false; 168 | } 169 | 170 | return Type::matches($type, $value); 171 | } 172 | 173 | /** 174 | * Given an array of default arguments, determine if the invalid 175 | * arguments can be resolved by swapping in their corresponding default 176 | * 177 | * @param array $defaults 178 | * @return bool 179 | */ 180 | public function canOverride($defaults) { 181 | if (empty($this->invalid_args)) return true; 182 | if (empty($defaults)) return false; 183 | return (count(array_intersect_key ( $this->invalid_args, $defaults )) == count($this->invalid_args) ); 184 | } 185 | 186 | /** 187 | * Given an array of overrides, replaces existing elements in $args 188 | * with the corresponding element in $defaults and returns the 189 | * modified $args array 190 | * 191 | * Can be used when validation fails to set invalid arguments to 192 | * their default as a fallback 193 | * 194 | * @param array $args 195 | * @param array $defaults 196 | * @param array $overrides 197 | * @return array 198 | */ 199 | protected function defaultOverride(array $args, array $defaults, 200 | array $overrides) { 201 | foreach($overrides as $override) { 202 | if (!empty($defaults[$override])) { 203 | $args[$override] = $defaults[$override]; 204 | } 205 | } 206 | return $args; 207 | } 208 | 209 | /** 210 | * Parses a string representation of a rule and returns an array 211 | * of valid types for that rule. 212 | * 213 | * @param string $str 214 | * @return array 215 | */ 216 | public function parseRuleString($str) { 217 | if (is_array($str)) return $str; 218 | return explode('|', $str); 219 | } 220 | 221 | /** 222 | * Add error message to the errors array 223 | * 224 | * @param string $arg 225 | * @param string $expected 226 | * @param string $got 227 | * @return void 228 | */ 229 | private function addTypeError($arg, $expected, $got) { 230 | $this->errors[] = sprintf("Invalid argument '%s'. Expected type '%s'". 231 | " but got '%s'.", $arg, $expected, $got); 232 | } 233 | 234 | 235 | /** 236 | * Boolean function indicating whether the validation test passed. 237 | * Returns true if validation passed, false otherwise. 238 | * 239 | * @return bool 240 | */ 241 | public function passes() { 242 | return ($this->passed == true); 243 | } 244 | 245 | /** 246 | * Boolean function indicating whether the validation test failed. 247 | * Returns true if validation failed, false otherwise. 248 | * 249 | * @return bool 250 | */ 251 | public function fails() { 252 | return ($this->passed == false); 253 | } 254 | 255 | /** 256 | * Return an array of errors 257 | * 258 | * @return array 259 | */ 260 | public function getErrors() { 261 | return $this->errors; 262 | } 263 | 264 | /** 265 | * Return an array of invalid arguments 266 | * 267 | * @return array 268 | */ 269 | public function getInvalidArgs() { 270 | return $this->invalid_args; 271 | } 272 | 273 | /** 274 | * @return array 275 | */ 276 | public function getArgs() 277 | { 278 | return $this->args; 279 | } 280 | 281 | 282 | 283 | } 284 | -------------------------------------------------------------------------------- /src/Factory.php: -------------------------------------------------------------------------------- 1 | 'default', 23 | 'form' => array(), 24 | 'fields' => array(), 25 | 'wp_query' => array(), 26 | 'taxonomy_relation' => Relation::_AND, 27 | 'meta_key_relation' => Relation::_AND, 28 | 'debug' => false, 29 | 'debug_level' => 'log' 30 | ); 31 | 32 | protected static $rules = array( 33 | 'form' => 'array', 34 | 'fields' => 'array', 35 | 'wp_query' => 'array', 36 | 'taxonomy_relation' => 'Relation', 37 | 'meta_key_relation' => 'Relation', 38 | 'debug' => 'bool', 39 | 'debug_level' => 'string' 40 | ); 41 | 42 | public function __construct($args, $request = null) { 43 | $args = self::validate($args); 44 | $this->args = $this->preProcessArgs($args); 45 | $this->wp_query_args = $this->args['wp_query']; 46 | $this->errors = array(); 47 | $this->inputs = array(); 48 | $this->fields_ready = false; 49 | $this->request = $this->processRequest($request); 50 | $this->fields = $this->initFieldTable(); 51 | $this->initFields(); 52 | $this->buildForm(); 53 | } 54 | 55 | /** 56 | * Creates empty table of fields as $field_type => FieldGroup pairs 57 | * 58 | * @return array 59 | */ 60 | private function initFieldTable() { 61 | $table = array(); 62 | $field_types = FieldType::getConstants(); 63 | foreach ($field_types as $type) { 64 | $field_group = new FieldGroup(); 65 | $field_group->setRelation($this->getFieldRelation($type)); 66 | $table[$type] = $field_group; 67 | } 68 | return $table; 69 | } 70 | 71 | /** 72 | * Get relation for a given field type 73 | * 74 | * @param $field_type 75 | * @return string 76 | */ 77 | private function getFieldRelation($field_type) { 78 | $include = array('meta_key' => 1, 'taxonomy' => 1); 79 | if (isset($include[$field_type])) return $this->args[$field_type.'_relation']; 80 | return Relation::_default; 81 | } 82 | 83 | /** 84 | * Perform initial processing of arguments 85 | * 86 | * @param $args 87 | * @return array 88 | */ 89 | private function preProcessArgs($args) { 90 | global $post; 91 | 92 | if (!is_array($args)) return array(); 93 | 94 | if (empty($args['form']['action']) && is_object($post) && isset($post->ID)) { 95 | $args['form']['action'] = get_permalink($post->ID); 96 | } 97 | 98 | $args = self::parseArgs($args, self::$defaults); 99 | 100 | return $args; 101 | } 102 | 103 | /** 104 | * Process and sanitize an array of request variables 105 | * 106 | * @param $request 107 | * @return mixed 108 | */ 109 | private function processRequest($request) { 110 | $data = (empty($request)) ? $this->getRequestGlobal($this->args['form']) : $request; 111 | return new HttpRequest($data); 112 | } 113 | 114 | /** 115 | * Populate fields table 116 | */ 117 | private function initFields() { 118 | if (empty($this->args['fields'])) return; 119 | foreach ($this->args['fields'] as $f) { 120 | try { 121 | $field = new Field($f); 122 | } catch (\Exception $e) { 123 | $this->addExceptionError($e); 124 | continue; 125 | } 126 | $field_group = $this->fields[$field->getFieldType()]; 127 | $field_group->addField($field); 128 | $this->addInputs($field, $this->request); 129 | } 130 | $this->fields_ready = true; 131 | } 132 | 133 | /** 134 | * Given a Field object, initializes that field's input(s) to Input objects 135 | * and adds them to the inputs table 136 | * 137 | * @param Field $field 138 | * @param $request 139 | */ 140 | private function addInputs(Field $field, $request) { 141 | $field_type = $field->getFieldType(); 142 | $inputs = $field->getInputs(); 143 | 144 | foreach ($inputs as $name => $input_args) { 145 | try { 146 | if ($field_type == FieldType::date) { 147 | $date_type = (!empty($input_args['date_type'])) ? $input_args['date_type'] : false; 148 | $post_types = $this->selectedPostTypes($request); 149 | $name = RequestVar::nameToVar($name, $field_type, $date_type); 150 | $input = DateInputBuilder::make($name, $input_args, $post_types, $request); 151 | } else { 152 | if ($field_type == FieldType::generic) 153 | $name = empty($input_args['id']) ? 'generic' : $input_args['id']; 154 | else 155 | $name = RequestVar::nameToVar($name, $field_type); 156 | $input = InputBuilder::make($name, $field_type, $input_args, $request); 157 | } 158 | $this->inputs[] = $input; 159 | } catch (\InvalidArgumentException $e) { 160 | $this->addExceptionError($e); 161 | continue; 162 | } catch (\Exception $e) { 163 | $this->addExceptionError($e); 164 | continue; 165 | } 166 | } 167 | } 168 | 169 | /** 170 | * Initializes a new Form object and adds all inputs to it 171 | */ 172 | private function buildForm() { 173 | 174 | try { 175 | $this->form = new Form($this->args['wpas_id'], $this->args['form']); 176 | } catch (\Exception $e) { 177 | $this->addExceptionError($e); 178 | return; 179 | } 180 | 181 | foreach ($this->inputs as $input) { 182 | $this->form->addInput($input); 183 | } 184 | 185 | } 186 | 187 | /** 188 | * Initializes and returns a WP_Query object for the search instance 189 | * 190 | * @return \WP_Query 191 | */ 192 | public function buildQueryObject() { 193 | $query = new Query($this->fields, $this->wp_query_args, $this->request); 194 | $query = new \WP_Query($query->getArgs()); 195 | $query->query_vars['post_type'] = (empty($this->wp_query_args['post_type'])) ? 'post' : $this->wp_query_args['post_type']; 196 | 197 | include_once( ABSPATH . 'wp-admin/includes/plugin.php' ); 198 | $search_query = $this->request->get(RequestVar::search); 199 | if (!empty($search_query) && $this->relevanssiActive()) { 200 | relevanssi_do_query($query); 201 | } 202 | 203 | $this->wp_query_obj = $query; 204 | return $query; 205 | } 206 | 207 | /** 208 | * Adds information abou an exception-based error to the object's error list 209 | * 210 | * @param $exception 211 | */ 212 | private function addExceptionError($exception) { 213 | $error = array(); 214 | $error['msg'] = $exception->getMessage(); 215 | $error['trace'] = $exception->getTraceAsString(); 216 | 217 | $this->errors[] = $error; 218 | } 219 | 220 | /** 221 | * Returns an array containing the post types currently being queried 222 | * 223 | * @param HttpRequest $request 224 | * @return array 225 | */ 226 | private function selectedPostTypes(HttpRequest $request) { 227 | $wp_query = $this->wp_query_args; 228 | $val = $request->get(RequestVar::post_type); 229 | 230 | if (!empty($request) && !empty($val)) { 231 | $post_types = $val; 232 | } else if (!empty($wp_query) && !empty($wp_query['post_type'])) { 233 | $post_types = $wp_query['post_type']; 234 | } else { 235 | $post_types = array(); 236 | } 237 | if (!is_array($post_types)) $post_types = array($post_types); 238 | return $post_types; 239 | } 240 | 241 | /** 242 | * Returns the superglobal corresponding to the current form's specified 243 | * method (eg POST or GET) 244 | * 245 | * @param $form_args 246 | * @return mixed 247 | */ 248 | private function getRequestGlobal($form_args) { 249 | if (!empty($form_args['method']) && $form_args['method'] == 'POST') { 250 | return $_POST; 251 | } 252 | return $_GET; 253 | } 254 | 255 | /** 256 | * @return bool 257 | */ 258 | private function relevanssiActive() { 259 | include_once( ABSPATH . 'wp-admin/includes/plugin.php' ); 260 | return (is_plugin_active('relevanssi/relevanssi.php') || is_plugin_active('relevanssi-premium/relevanssi.php')); 261 | } 262 | 263 | /** 264 | * @return bool 265 | */ 266 | public function hasErrors() 267 | { 268 | return (!empty($this->errors)); 269 | } 270 | 271 | /** 272 | * @return Form 273 | */ 274 | public function getForm() 275 | { 276 | return $this->form; 277 | } 278 | 279 | /** 280 | * @return array 281 | */ 282 | public function getFields() 283 | { 284 | return $this->fields; 285 | } 286 | 287 | /** 288 | * @return array 289 | */ 290 | public function getErrors() 291 | { 292 | return $this->errors; 293 | } 294 | 295 | /** 296 | * @return array 297 | */ 298 | public function getInputs() 299 | { 300 | return $this->inputs; 301 | } 302 | 303 | /** 304 | * @return array 305 | */ 306 | public function getWPQuery() 307 | { 308 | return $this->wp_query_obj; 309 | } 310 | 311 | /** 312 | * @return HttpRequest 313 | */ 314 | public function getRequest() 315 | { 316 | return $this->request; 317 | } 318 | 319 | } -------------------------------------------------------------------------------- /js/scripts.js: -------------------------------------------------------------------------------- 1 | // Global constants 2 | var __WPAS = { 3 | FORM : "#wp-advanced-search.wpas-ajax-enabled", 4 | CONTAINER : "#wpas-results", 5 | INNER : "#wpas-results-inner", 6 | DEBUG_CONTAINER : "#wpas-debug", 7 | PAGE_FIELD : "#wpas-paged", 8 | FORM_ID: "", 9 | KEY_PREFIX: "wpasInstance_", 10 | HASH: "", 11 | STORAGE_KEY: function() { 12 | return this.KEY_PREFIX + this.FORM_ID; 13 | } 14 | }; 15 | 16 | jQuery(document).ready(function($) { 17 | 18 | __WPAS.FORM_ID = $('#wpas-id').val(); 19 | __WPAS.HASH = $(__WPAS.FORM).data('ajax-url-hash'); 20 | var CURRENT_PAGE = 1; 21 | 22 | /** 23 | * Event listeners 24 | */ 25 | 26 | $('form.wpas-autosubmit :input').change(function() { 27 | $(this).parents('form').submit(); 28 | }); 29 | 30 | $('button.wpas-clear').click(function(e) { 31 | e.preventDefault(); 32 | $(this).parents('form').find(':input') 33 | .not(':button, :submit, :reset, :hidden') 34 | .val('') 35 | .removeAttr('checked') 36 | .removeAttr('selected'); 37 | $(this).parents('form.wpas-autosubmit').each(function() { 38 | $(this).submit(); 39 | return false; 40 | }); 41 | }); 42 | 43 | $('input.wpas-reset').click(function(e){ 44 | e.preventDefault(); 45 | $(this).parents('form')[0].reset(); 46 | $(this).parents('form.wpas-autosubmit').each(function() { 47 | $(this).submit(); 48 | return false; 49 | }); 50 | }); 51 | 52 | /** 53 | * AJAX Functionality 54 | */ 55 | 56 | if ($(__WPAS.FORM).length == 0) { 57 | log("No AJAX-enabled WPAS search form detected on page."); 58 | return; 59 | } 60 | 61 | if ($(__WPAS.CONTAINER).length == 0) { 62 | log("No container with ID #wpas-results found on page. Results cannot be shown"); 63 | return; 64 | } 65 | 66 | var DEBUG_ON = ($(__WPAS.FORM).hasClass('wpas-debug-enabled')) ? true : false; 67 | var SHOW_DEFAULT = ($(__WPAS.FORM).data('ajax-show-default')) ? true : false; 68 | 69 | var T = (DEBUG_ON) ? 500 : 0; 70 | 71 | if (DEBUG_ON && $(__WPAS.DEBUG_CONTAINER).length == 0) { 72 | log("WPAS_DEBUG is enabled but no container with ID #wpas-debug was found " + 73 | "on this page. Debug information cannot be shown."); 74 | return; 75 | } 76 | 77 | var ajaxLoader = { 78 | container: "wpas-load", 79 | load_btn: "wpas-load-btn", 80 | load_btn_text: "", 81 | load_img: "wpas-loading-img", 82 | load_img_url: "", 83 | init : function(form) { 84 | this.load_btn_text = $(form).data('ajax-button'); 85 | this.load_img_url = $(form).data('ajax-loading'); 86 | $(__WPAS.CONTAINER).append(this.create()); 87 | }, 88 | 89 | create: function() { 90 | var html = "
"; 91 | html += "
"; 92 | html += "
"; 93 | html += "
"; 94 | return html; 95 | }, 96 | 97 | showButton: function() { 98 | $('#'+this.load_btn).addClass('active').show(); 99 | }, 100 | 101 | hideButton: function() { 102 | $('#'+this.load_btn).removeClass('active').hide(); 103 | }, 104 | 105 | showImage: function() { 106 | $('#'+this.load_img).show(); 107 | }, 108 | 109 | hideImage: function() { 110 | $('#'+this.load_img).hide(); 111 | } 112 | 113 | }; 114 | 115 | $(__WPAS.CONTAINER).append("
"); 116 | ajaxLoader.init(__WPAS.FORM); 117 | 118 | var storage = null; 119 | if (window.location.hash.slice(1) == __WPAS.HASH) { 120 | storage = JSON.parse(localStorage.getItem("wpasInstance_"+__WPAS.FORM_ID)); 121 | } 122 | 123 | if (storage != null) { 124 | log("localStorage found"); 125 | loadInstance(); 126 | } else { 127 | setPage(1); 128 | setRequest($(__WPAS.FORM).serialize()); 129 | } 130 | 131 | if ($(__WPAS.CONTAINER).length != 0) { 132 | if (storage != null) { 133 | $(__WPAS.CONTAINER).html(storage.results); 134 | } else if (SHOW_DEFAULT) { // Show results by default if attribute is set 135 | sendRequest($(__WPAS.FORM).serialize(), CURRENT_PAGE); 136 | } 137 | } 138 | 139 | // Submits the form 140 | // Reset current page to 1 141 | function submitForm(form) { 142 | setPage(1); 143 | var form_data = $(form).serialize(); 144 | setRequest(form_data); 145 | $(__WPAS.INNER).empty(); 146 | sendRequest(form_data, CURRENT_PAGE); 147 | } 148 | 149 | // Set AJAX request to fetch results 150 | // Appends results to the container 151 | function sendRequest(data, page) { 152 | ajaxLoader.hideButton(); 153 | ajaxLoader.showImage(); 154 | jQuery.ajax({ 155 | type: 'POST', 156 | url: WPAS_Ajax.ajaxurl, 157 | data: { 158 | action: 'wpas_ajax_load', 159 | page: page, 160 | form_data: data 161 | }, 162 | success: function(data, textStatus, XMLHttpRequest) { 163 | response = JSON.parse(data); 164 | setTimeout(function() { 165 | appendHTML(__WPAS.INNER, response.results); 166 | ajaxLoader.hideImage(); 167 | updateHTML(__WPAS.DEBUG_CONTAINER,response.debug); 168 | CURRENT_PAGE = response.current_page; 169 | var max_page = response.max_page; 170 | 171 | log("Current Page: "+CURRENT_PAGE+", Max Page: "+max_page); 172 | 173 | if (max_page == 0 || CURRENT_PAGE == max_page) { 174 | ajaxLoader.hideButton(); 175 | } else { 176 | ajaxLoader.showButton(); 177 | } 178 | 179 | window.location.hash = __WPAS.HASH; 180 | storeInstance(); 181 | unlockForm(); 182 | 183 | }, T); 184 | 185 | }, 186 | error: function(MLHttpRequest, textStatus, errorThrown){ 187 | console.log(errorThrown); 188 | } 189 | }); 190 | } 191 | 192 | function storeInstance() { 193 | var instance = { request: REQUEST_DATA, form: getFormValues(), results : getResults(), page: CURRENT_PAGE }; 194 | instance = JSON.stringify(instance); 195 | localStorage.setItem(__WPAS.STORAGE_KEY(), instance); 196 | } 197 | 198 | function addArrayValues(values, input) { 199 | var name = $(input).attr('name'); 200 | var value = $(input).val(); 201 | 202 | if (typeof values[name] == 'undefined') { 203 | values[name] = []; 204 | } 205 | 206 | if ($(input).is(":checked")) values[name].push(value); 207 | 208 | return values; 209 | } 210 | 211 | function getFormValues() { 212 | var values = {}; 213 | $(__WPAS.FORM).find(':input').not(':button, :submit, :reset').each(function() { 214 | if ($(this).attr('type') == 'checkbox') { 215 | values = addArrayValues(values, this) 216 | } else { 217 | values[$(this).attr('name')] = $(this).val(); 218 | } 219 | }); 220 | return values; 221 | } 222 | 223 | function getResults() { 224 | return $(__WPAS.CONTAINER).html(); 225 | } 226 | 227 | function loadInstance() { 228 | var instance = localStorage.getItem(__WPAS.STORAGE_KEY()); 229 | instance = JSON.parse(instance); 230 | if (instance == null) return; 231 | if (instance.form) loadForm(instance.form); 232 | if (instance.results) loadResults(instance.results); 233 | if (instance.page) setPage(instance.page); 234 | if (instance.request) setRequest(instance.request); 235 | } 236 | 237 | function loadForm(form_values) { 238 | $(__WPAS.FORM).find(':input').not(':button, :submit, :reset').each(function() { 239 | var value = form_values[$(this).attr('name')]; 240 | if ($(this).attr('type') == 'checkbox') { 241 | if (value.indexOf( $(this).val() ) >= 0) { 242 | $(this).prop('checked',true); 243 | } else { 244 | $(this).prop('checked',false); 245 | } 246 | } else { 247 | $(this).val(value); 248 | } 249 | }); 250 | } 251 | 252 | function loadResults(results) { 253 | $(__WPAS.CONTAINER).html(results); 254 | } 255 | 256 | function lockForm() { 257 | $(__WPAS.FORM).addClass('wpas-locked'); 258 | $(__WPAS.FORM).find('input:submit').attr('disabled', 'disabled'); 259 | } 260 | 261 | function formLocked() { 262 | return $(__WPAS.FORM).hasClass('wpas-locked'); 263 | } 264 | 265 | function unlockForm() { 266 | $(__WPAS.FORM).removeClass('wpas-locked'); 267 | $(__WPAS.FORM).find('input:submit').removeAttr('disabled'); 268 | } 269 | 270 | function appendHTML(el, content) { 271 | $(el).append(content); 272 | } 273 | 274 | function updateHTML(el, content) { 275 | $(el).html(content); 276 | } 277 | 278 | function setPage(pagenum) { 279 | CURRENT_PAGE = pagenum; 280 | $(__WPAS.PAGE_FIELD).val(pagenum); 281 | } 282 | 283 | function setRequest(request) { 284 | REQUEST_DATA = request; 285 | } 286 | 287 | function log(msg) { 288 | if (DEBUG_ON) console.log("WPAS: " + msg); 289 | } 290 | 291 | // AJAX Event Listeners 292 | 293 | $(__WPAS.FORM).submit(function(e) { 294 | e.preventDefault(); 295 | if (formLocked()) return; 296 | lockForm(); 297 | submitForm(this); 298 | }); 299 | 300 | $(document).on('click', '#'+ajaxLoader.load_btn+'.active', function(e){ 301 | setPage(parseInt(CURRENT_PAGE) + 1) 302 | sendRequest(REQUEST_DATA,CURRENT_PAGE); 303 | }); 304 | 305 | }); -------------------------------------------------------------------------------- /src/InputBuilder.php: -------------------------------------------------------------------------------- 1 | get($request_var); 128 | 129 | if (isset($request_val)) { 130 | if (is_array($request_val)) { 131 | return $request_val ; 132 | } 133 | return array($request_val); 134 | } 135 | 136 | if (self::canApplyDefaultAll($args, $request)) { 137 | $selected = array(); 138 | foreach ($args['values'] as $value => $label) { 139 | $selected[] = $value; 140 | } 141 | return $selected; 142 | } 143 | 144 | if (isset($args['default']) && self::isFormSubmitted($request) === false) { 145 | if (!is_array($args['default'])) { 146 | return array($args['default']); 147 | } 148 | return $args['default']; 149 | } 150 | 151 | return array(); 152 | } 153 | 154 | /** 155 | * Determine whether the default_all option should be invoked under 156 | * the given arguments and request data 157 | * 158 | * @param $args 159 | * @param $request 160 | * @return bool 161 | */ 162 | protected static function canApplyDefaultAll($args, $request) { 163 | $format = isset($args['format']) ? $args['format'] : false; 164 | $default_all = isset($args['default_all']) ? $args['default_all'] : false; 165 | $supports_multiple = ($format == 'checkbox' || $format == 'multi-select'); 166 | 167 | return ($default_all && $supports_multiple && self::isFormSubmitted($request) === false); 168 | } 169 | 170 | /** 171 | * Generates a search field 172 | */ 173 | public static function search($args) { 174 | $defaults = array( 175 | 'label' => '', 176 | 'format' => 'text', 177 | 'values' => array() 178 | ); 179 | $args = self::parseArgs($args, $defaults); 180 | 181 | return $args; 182 | } 183 | 184 | /** 185 | * Generates a submit button 186 | */ 187 | public static function submit($args) { 188 | $defaults = array( 189 | 'values' => array('Search') 190 | ); 191 | $args = self::parseArgs($args, $defaults); 192 | $args['format'] = 'submit'; 193 | return $args; 194 | } 195 | 196 | /** 197 | * Generates a reset button 198 | */ 199 | public static function reset($args) { 200 | $defaults = array( 201 | 'values' => array('Reset') 202 | ); 203 | $args = self::parseArgs($args, $defaults); 204 | $args['format'] = 'reset'; 205 | return $args; 206 | } 207 | 208 | /** 209 | * Generates a clear button 210 | */ 211 | public static function clear($args) { 212 | $defaults = array( 213 | 'values' => array('Clear') 214 | ); 215 | $args = self::parseArgs($args, $defaults); 216 | $args['format'] = 'clear'; 217 | return $args; 218 | } 219 | 220 | /** 221 | * Configure a meta_key input 222 | * 223 | * @param $args 224 | * @return array 225 | */ 226 | public static function meta_key($args) { 227 | $defaults = array( 228 | 'label' => '', 229 | 'format' => 'select', 230 | 'data_type' => 'CHAR', 231 | 'compare' => 'IN', 232 | 'values' => array() 233 | ); 234 | $args = self::parseArgs($args, $defaults); 235 | return $args; 236 | } 237 | 238 | /** 239 | * Configure an order input 240 | * 241 | * @param $args 242 | * @return array 243 | */ 244 | public static function order($args) { 245 | $defaults = array( 246 | 'label' => '', 247 | 'format' => 'select', 248 | 'values' => array('ASC' => 'ASC', 'DESC' => 'DESC') 249 | ); 250 | 251 | $args = self::parseArgs($args, $defaults); 252 | return $args; 253 | } 254 | 255 | /** 256 | * Configure an orderby input 257 | * 258 | * @param $args 259 | * @return array 260 | */ 261 | public static function orderby($args) { 262 | $defaults = array( 263 | 'label' => '', 264 | 'format' => 'select', 265 | 'values' => array( 'ID' => 'ID', 266 | 'author' => 'Author', 267 | 'title' => 'Title', 268 | 'date' => 'Date', 269 | 'modified' => 'Modified', 270 | 'parent' => 'Parent ID', 271 | 'rand' => 'Random', 272 | 'comment_count' => 'Comment Count', 273 | 'menu_order' => 'Menu Order' ) 274 | ); 275 | 276 | if (isset($args['orderby_values']) && is_array($args['orderby_values'])) { 277 | $args['values'] = array(); // orderby_values overrides normal values 278 | foreach ($args['orderby_values'] as $k=>$v) { 279 | if (isset($v['label'])) $label = $v['label']; 280 | else $label = $k; 281 | $args['values'][$k] = $label; // add to the values array 282 | } 283 | } 284 | $args = self::parseArgs($args, $defaults); 285 | return $args; 286 | } 287 | 288 | /** 289 | * Configure an author input 290 | * 291 | * @param $args 292 | * @return array 293 | */ 294 | public static function author($args) { 295 | return AuthorArgParser::parse($args); 296 | } 297 | 298 | /** 299 | * Configure a post_type input 300 | * 301 | * @param $args 302 | * @return array 303 | */ 304 | public static function post_type($args) { 305 | $defaults = array( 306 | 'label' => '', 307 | 'format' => 'select', 308 | 'values' => array('post' => 'Post', 'page' => 'Page') 309 | ); 310 | $args = self::parseArgs($args, $defaults); 311 | $values = $args['values']; 312 | 313 | if (count($values) < 1) { 314 | $post_types = get_post_types(array('public' => true)); 315 | foreach ( $post_types as $post_type ) { 316 | $obj = get_post_type_object($post_type); 317 | $post_type_id = $obj->name; 318 | $post_type_name = $obj->labels->name; 319 | $values[$post_type_id] = $post_type_name; 320 | } 321 | } 322 | 323 | $args['values'] = $values; 324 | return $args; 325 | } 326 | 327 | /** 328 | * Configure an html input 329 | * 330 | * @param $args 331 | * @return array 332 | */ 333 | public static function html($args) { 334 | $defaults = array( 335 | 'label' => '', 336 | 'values' => array() 337 | ); 338 | $args = self::parseArgs($args, $defaults); 339 | $args['format'] = 'html'; 340 | 341 | return $args; 342 | } 343 | 344 | /** 345 | * Configure a generic input 346 | * 347 | * @param $args 348 | * @return array 349 | */ 350 | public static function generic($args) { 351 | return $args; 352 | } 353 | 354 | /** 355 | * Configure a posts_per_page input 356 | * 357 | * @param $args 358 | * @return array 359 | */ 360 | public static function posts_per_page($args) { 361 | $defaults = array( 362 | 'format' => 'select', 363 | 'values' => array(10 => "10", 25 => "25", 50 => "50") 364 | ); 365 | $args = self::parseArgs($args, $defaults); 366 | return $args; 367 | } 368 | 369 | /** 370 | * Configure a taxonomy input 371 | * @param $args 372 | * @return array 373 | */ 374 | public static function taxonomy($args) { 375 | return TaxonomyArgParser::parse($args); 376 | } 377 | 378 | private static function isFormSubmitted(HttpRequest $request) { 379 | $val = $request->get('wpas_submit'); 380 | return isset($val); 381 | } 382 | 383 | } -------------------------------------------------------------------------------- /tests/TestValidator.php: -------------------------------------------------------------------------------- 1 | 1, 11 | 'item2' => "a string", 12 | 'item3' => array("test"), 13 | 'item4' => false); 14 | 15 | $validation = new Validator($rules, $data); 16 | $this->assertTrue($validation->passes()); 17 | } 18 | 19 | public function testPassesWithNoData() { 20 | $data = array(); 21 | $rules = array( 22 | 'item' => 1, 23 | 'item2' => "a string", 24 | 'item3' => array("test"), 25 | 'item4' => false); 26 | 27 | $validation = new Validator($rules, $data); 28 | $this->assertTrue($validation->passes(), $validation->getErrors()); 29 | } 30 | 31 | public function testPassesWithMissingArgValue() { 32 | $rules = array('item4' => 'string'); 33 | $data = array( 34 | 'item' => 1, 35 | 'item2' => "a string", 36 | 'item3' => array("test"), 37 | 'item4'); 38 | 39 | $validation = new Validator($rules, $data); 40 | $this->assertTrue($validation->passes()); 41 | } 42 | 43 | public function testFailsWithMissingRequiredValue() { 44 | $rules = array('item4' => array('type' => 'string', 'required' => true)); 45 | $data = array( 46 | 'item' => 1, 47 | 'item2' => "a string", 48 | 'item3' => array("test"), 49 | 'item4'); 50 | 51 | $validation = new Validator($rules, $data); 52 | $this->assertFalse($validation->passes()); 53 | 54 | $data = array( 55 | 'item' => 1, 56 | 'item2' => "a string", 57 | 'item3' => array("test")); 58 | $validation = new Validator($rules, $data); 59 | $this->assertFalse($validation->passes()); 60 | } 61 | 62 | public function testPassesWithMissingNonRequiredValue() { 63 | $rules = array('item4' => array('type' => 'string', 'required' => false)); 64 | $data = array( 65 | 'item' => 1, 66 | 'item2' => "a string", 67 | 'item3' => array("test")); 68 | 69 | $validation = new Validator($rules, $data); 70 | $this->assertTrue($validation->passes()); 71 | } 72 | 73 | public function testInvalidArgs() { 74 | $rules = array('item4' => array('type' => 'string', 'required' => true)); 75 | $data = array( 76 | 'item' => 1, 77 | 'item2' => "a string", 78 | 'item3' => array("test")); 79 | $validation = new Validator($rules, $data); 80 | $i = $validation->getInvalidArgs(); 81 | 82 | $this->assertTrue(count($i) == 1); 83 | $this->assertTrue(isset($i['item4'])); 84 | } 85 | 86 | public function testRequiredString() { 87 | $rules = array('item4' => 'required'); 88 | $data = array( 89 | 'item' => 1, 90 | 'item2' => "a string", 91 | 'item3' => array("test")); 92 | 93 | $validation = new Validator($rules, $data); 94 | $this->assertFalse($validation->passes()); 95 | 96 | 97 | $data = array( 98 | 'item' => 1, 99 | 'item2' => "a string", 100 | 'item4' => array("test")); 101 | $validation = new Validator($rules, $data); 102 | $this->assertTrue($validation->passes()); 103 | } 104 | 105 | public function testMatches() { 106 | $rules = array('item4' => array('matches' => 'dog|cat')); 107 | $data = array( 108 | 'item' => 1, 109 | 'item2' => "a string", 110 | 'item4' => 'cat' 111 | ); 112 | 113 | $validation = new Validator($rules, $data); 114 | $this->assertTrue($validation->passes()); 115 | 116 | $rules = array('item4' => array('matches' => 'dog|CAT')); 117 | $data = array( 118 | 'item' => 1, 119 | 'item2' => "a string", 120 | 'item4' => 'cat' 121 | ); 122 | 123 | $validation = new Validator($rules, $data); 124 | $this->assertTrue($validation->passes()); 125 | 126 | $rules = array('item4' => array('matches' => 'dog|cat')); 127 | $data = array( 128 | 'item' => 1, 129 | 'item2' => "a string", 130 | 'item4' => 'fish' 131 | ); 132 | 133 | $validation = new Validator($rules, $data); 134 | $this->assertFalse($validation->passes()); 135 | } 136 | 137 | 138 | public function testTypeChecking() { 139 | $types = array( 'string' => 'hello', 140 | 'numeric' => 3, 141 | 'bool' => true, 142 | 'array' => array(3, 'hello', false, array(1,2,3)), 143 | 'array' => array('one', 'two'), 144 | 'array' => array(3, 'hello', false) ); 145 | foreach($types as $expect => $expect_value) { 146 | foreach($types as $got => $got_value) { 147 | 148 | $msg = sprintf("Testing expect=%s vs got=%s",$expect, $got); 149 | 150 | $validation = new Validator( 151 | array('arg' => $expect), 152 | array('arg' => $got_value) 153 | ); 154 | 155 | $msg .= " " . implode(" ",$validation->getErrors()); 156 | 157 | if (Type::isSubtypeOf($got, $expect)) { 158 | $this->assertTrue($validation->passes(), $msg); 159 | } else { 160 | if ($validation->passes()) 161 | //echo sprintf("%s is NOT a subtype of %s", $got, $expect); 162 | $this->assertTrue($validation->fails(), $msg); 163 | } 164 | } 165 | } 166 | } 167 | 168 | public function testScalarTypeChecking() { 169 | $values = array("mystring", 15, 0.245, false); 170 | foreach($values as $value) { 171 | $msg = sprintf("Testing expect=%s, got value=%s","scalar", $value); 172 | $validation = new Validator ( 173 | array('arg' => 'scalar'), 174 | array('arg' => $value) 175 | ); 176 | $this->assertTrue($validation->passes(), $msg); 177 | } 178 | 179 | $msg = sprintf("Testing expect=%s vs got=array","scalar", $value); 180 | $validation = new Validator( 181 | array('arg' => 'scalar'), 182 | array('arg' => array("one", "two")) 183 | ); 184 | 185 | $this->assertFalse($validation->passes(), $msg); 186 | } 187 | 188 | public function testTypedArrayChecked() { 189 | $tests = array( 190 | 191 | array( "expect" => "array", 192 | "got" => array("one", "two"), 193 | "valid" => true), 194 | array( "expect" => "array", 195 | "got" => array("one", 321), 196 | "valid" => true), 197 | array( "expect" => "array", 198 | "got" => array(0.235, array("something")), 199 | "valid" => true), 200 | 201 | array( "expect" => "array", 202 | "got" => array("one", "two"), 203 | "valid" => true), 204 | array( "expect" => "array", 205 | "got" => array("one", "two", 567), 206 | "valid" => false), 207 | array( "expect" => "array", 208 | "got" => array("one", "two", array("one")), 209 | "valid" => false), 210 | array( "expect" => "array", 211 | "got" => array(true, false), 212 | "valid" => false), 213 | 214 | array( "expect" => "array", 215 | "got" => array(1, 2), 216 | "valid" => true), 217 | array( "expect" => "array", 218 | "got" => array(1.34, 2), 219 | "valid" => true), 220 | array( "expect" => "array", 221 | "got" => array("one", "two", 567), 222 | "valid" => false), 223 | array( "expect" => "array", 224 | "got" => array("one", "two", array("one")), 225 | "valid" => false), 226 | array( "expect" => "array", 227 | "got" => array(true, false), 228 | "valid" => false), 229 | 230 | ); 231 | 232 | foreach ($tests as $test) { 233 | $validation = new Validator( 234 | array('arg' => $test['expect']), 235 | array('arg' => $test['got']) 236 | ); 237 | 238 | $msg = sprintf("Testing %s with value=[%s], valid=%b. Error: %s", 239 | $test['expect'], json_encode($test['got']), 240 | $test['valid'], implode(" ",$validation->getErrors())); 241 | 242 | $this->assertEquals($test['valid'], $validation->passes(), $msg); 243 | } 244 | } 245 | 246 | public function testMixedTypes() { 247 | $tests = array( 248 | 249 | array( "expect" => "array|bool|numeric|string", 250 | "got" => array("one"), 251 | "valid" => true 252 | ), 253 | array( "expect" => "array|bool|numeric|string", 254 | "got" => false, 255 | "valid" => true 256 | ), 257 | array( "expect" => "array|bool|numeric|string", 258 | "got" => 123, 259 | "valid" => true 260 | ), 261 | array( "expect" => "array|bool|numeric|string", 262 | "got" => "mystring", 263 | "valid" => true 264 | ), 265 | array( "expect" => "array|numeric", 266 | "got" => array("one"), 267 | "valid" => true 268 | ), 269 | array( "expect" => "array|numeric", 270 | "got" => 123, 271 | "valid" => true 272 | ), 273 | array( "expect" => "array|numeric", 274 | "got" => "mystring", 275 | "valid" => false 276 | ), 277 | array( "expect" => "array|array", 278 | "got" => 123, 279 | "valid" => false 280 | ), 281 | array( "expect" => "array|array", 282 | "got" => array("one","two"), 283 | "valid" => true 284 | ), 285 | array( "expect" => "array|array", 286 | "got" => array(true,true,false), 287 | "valid" => true 288 | ), 289 | array( "expect" => "array|array", 290 | "got" => array(4.5,7.8,91.2), 291 | "valid" => false 292 | ), 293 | ); 294 | 295 | foreach ($tests as $test) { 296 | $validation = new Validator( 297 | array('arg' => $test['expect']), 298 | array('arg' => $test['got']) 299 | ); 300 | 301 | $got = (is_array($test['got'])) ? implode(",",$test['got']) 302 | : $test['got']; 303 | 304 | $msg = sprintf("Testing %s with value=[%s], valid=%b. Error: %s", 305 | $test['expect'], $got, 306 | $test['valid'], implode(" ",$validation->getErrors())); 307 | 308 | $this->assertEquals($test['valid'], $validation->passes(), $msg); 309 | } 310 | 311 | 312 | } 313 | 314 | } -------------------------------------------------------------------------------- /src/InputMarkup.php: -------------------------------------------------------------------------------- 1 | 1, 12 | 'color' => 1, 13 | 'date' => 1, 14 | 'datetime' => 1, 15 | 'datetime-local' => 1, 16 | 'email' => 1, 17 | 'month' => 1, 18 | 'number' => 1, 19 | 'range' => 1, 20 | 'search' => 1, 21 | 'tel' => 1, 22 | 'time' => 1, 23 | 'url' => 1, 24 | 'week' => 1 25 | ); 26 | 27 | public function __construct(Input $input) { 28 | $this->input = $input; 29 | } 30 | 31 | public function generate() { 32 | $output = ''; 33 | $format = str_replace('-','_',$this->input->getFormat()); 34 | 35 | if ($format != 'hidden') { 36 | $output .= $this->input->getPreHtml(); 37 | $output .= $this->openWrapper(); 38 | $output .= $this->inputLabel(); 39 | } 40 | 41 | $output .= $this->generateInnerMarkup($format); 42 | 43 | if ($format != 'hidden') { 44 | $output .= $this->closeWrapper(); 45 | $output .= $this->input->getPostHtml(); 46 | } 47 | 48 | return $output; 49 | } 50 | 51 | private function generateInnerMarkup($format) { 52 | if (isset(self::$input_types[$format])) { 53 | return $this->input($format); 54 | } 55 | return $this->$format(); 56 | } 57 | 58 | private function openWrapper() { 59 | $output = ''; 60 | $id = $this->input->getId(); 61 | if ($this->input->wrappersDisabled() === false) { 62 | $output .= '
'; 64 | } 65 | return $output; 66 | } 67 | 68 | private function closeWrapper() { 69 | $output = ''; 70 | if ($this->input->wrappersDisabled() === false) { 71 | $output .= '
'; 72 | } 73 | return $output; 74 | } 75 | 76 | private function inputLabel() { 77 | $output = ''; 78 | $label = $this->input->getLabel(); 79 | if ($label) { 80 | $output .= '
' . 81 | '
'; 82 | } 83 | return $output; 84 | } 85 | 86 | /** 87 | * Generates a select field 88 | */ 89 | private function select($multi = false) { 90 | 91 | $output = ''; 114 | return $output; 115 | } 116 | 117 | /** 118 | * Generates a checkbox field 119 | * 120 | */ 121 | private function checkbox() { 122 | return $this->listField(true); 123 | } 124 | 125 | /** 126 | * Generates a radio field 127 | * 128 | */ 129 | private function radio() { 130 | return $this->listField(false); 131 | } 132 | 133 | /** 134 | * Generates a list-style field (either checkboxes or radio buttons) 135 | * 136 | * @param bool $is_checkbox 137 | * @return string 138 | */ 139 | private function listField($is_checkbox = true) { 140 | $group_label = ($is_checkbox) ? 'checkboxes' : 'radio-buttons'; 141 | $option_func = ($is_checkbox) ? 'checkboxOption' : 'radioOption'; 142 | 143 | $output = '
'; 144 | 145 | if ($this->input->isNested()) { 146 | $output .= $this->buildOptionsList($this->input->getValues(), array($this, $option_func), 0, true); 147 | return $output; 148 | } 149 | 150 | foreach ($this->input->getValues() as $value => $label) { 151 | $output .= call_user_func(array($this,$option_func), $value, $label); 152 | } 153 | 154 | $output .= '
'; 155 | return $output; 156 | } 157 | 158 | /** 159 | * Builds and returns list of field options 160 | * 161 | * Used for select, checkbox, and radio fields. Supports nested 162 | * hierarchies of elements. 163 | * 164 | * @param array $elements 165 | * @param $field_func 166 | * @param int $level 167 | * @param bool $list_el 168 | * @return string 169 | */ 170 | private function buildOptionsList($elements = array(), $field_func, $level = 0, 171 | $list_el = false) { 172 | if (empty($elements)) return ""; 173 | 174 | $output = ""; 175 | $output .= ($list_el) ? '
    ' : ''; 176 | 177 | foreach($elements as $element) { 178 | $output .= call_user_func($field_func, $element['value'], $element['label'], $level); 179 | $output .= $this->buildOptionsList($element['children'], $field_func, $level+1, $list_el); 180 | } 181 | 182 | $output .= ($list_el) ? '
' : ''; 183 | 184 | return $output; 185 | } 186 | 187 | /** 188 | * Generates a text input field 189 | * 190 | * Also used to generate other HTML5 field types through use of $input_type 191 | * argument. 192 | * 193 | */ 194 | private function input( $input_type = 'text' ) { 195 | $value = $this->getInputValue(); 196 | $placeholder = ''; 197 | if ($this->input->getPlaceholder()) 198 | $placeholder = ' placeholder="'.$this->input->getPlaceholder().'"'; 199 | $output = 'attributesString().'>'; 200 | return $output; 201 | } 202 | 203 | /** 204 | * Generates a textarea field 205 | * 206 | */ 207 | private function textarea() { 208 | $value = $this->getInputValue(); 209 | $placeholder = ''; 210 | if ($this->input->getPlaceholder()) 211 | $placeholder = ' placeholder="'.$this->input->getPlaceholder().'"'; 212 | $output = ''; 213 | return $output; 214 | } 215 | 216 | /** 217 | * Generates a submit button 218 | * 219 | * 220 | */ 221 | private function submit() { 222 | return $this->buttonInput('submit'); 223 | } 224 | 225 | /** 226 | * Generates a reset button 227 | * 228 | * @since 1.4 229 | */ 230 | private function reset() { 231 | return $this->buttonInput('reset'); 232 | } 233 | 234 | private function buttonInput($type) { 235 | $values = $this->input->getValues(); 236 | $value = reset($values); 237 | $output = 'attributesString().'>'; 238 | return $output; 239 | } 240 | 241 | /** 242 | * Generates a clear button 243 | * 244 | * @since 1.4 245 | */ 246 | private function clear() { 247 | $values = $this->input->getValues(); 248 | $value = reset($values); 249 | $output = ''; 250 | return $output; 251 | } 252 | 253 | /** 254 | * Generates an html field 255 | */ 256 | private function html() { 257 | $values = $this->input->getValues(); 258 | return reset($values); 259 | } 260 | 261 | /** 262 | * Generates a hidden field 263 | */ 264 | private function hidden() { 265 | $values = $this->input->getValues(); 266 | $value = reset($values); 267 | $output = 'attributesString().'>'; 268 | return $output; 269 | } 270 | 271 | /** 272 | * Creates a string of HTML element attributes for the input 273 | */ 274 | private function attributesString() { 275 | $output = ""; 276 | if ($this->input->getAttributes()) { 277 | foreach($this->input->getAttributes() as $k => $v) { 278 | $output .= $k . '="'.$v.'" '; 279 | } 280 | } 281 | return $output; 282 | } 283 | 284 | /** 285 | * Obtains the value to use in the field. 286 | * 287 | * Used only for text & textarea inputs 288 | * 289 | * @since 1.3 290 | */ 291 | private function getInputValue() { 292 | $value = ''; 293 | $selected = $this->input->getSelected(); 294 | $values = $this->input->getValues(); 295 | 296 | if (!empty($selected)) { 297 | $value = reset($selected); 298 | } else if (!empty($values)) { 299 | $value = reset($values); 300 | } 301 | return $value; 302 | } 303 | 304 | /** 305 | * Generates a single option for a select field 306 | * 307 | * @since 1.3 308 | */ 309 | private function selectOption($value, $label, $level = 0) { 310 | $indent = ''; 311 | if ($level > 0) { 312 | for($i=0; $i<$level; $i++) { 313 | $indent .= "—"; 314 | } 315 | $indent .= ' '; 316 | } 317 | $output = ''; 322 | return $output; 323 | } 324 | 325 | /** 326 | * Generates a single option for a checkbox field 327 | * 328 | * @since 1.3 329 | */ 330 | private function checkboxOption($value, $label, $level = 0) { 331 | return $this->listOption($value, $label, 'checkbox'); 332 | } 333 | 334 | /** 335 | * Generates a single option for a radio field 336 | * 337 | * @since 1.3 338 | */ 339 | private function radioOption($value, $label, $level = 0) { 340 | return $this->listOption($value, $label, 'radio'); 341 | } 342 | 343 | /** 344 | * Generates a single field option for a checkbox or radio field 345 | * 346 | * @param $value 347 | * @param $label 348 | * @param string $type "checkbox" or "radio" 349 | * @return string 350 | */ 351 | private function listOption($value, $label, $type = 'checkbox') { 352 | $ctr = $this->ctr; 353 | $element = ($this->input->isNested()) ? 'li' : 'div'; 354 | $id = $this->input->getId(); 355 | $name = $this->input->getInputName(); 356 | $name .= ($type == 'checkbox') ? '[]' : ''; 357 | $output = '<'.$element.' class="wpas-'.$id.'-'.$type.'-'.$ctr.'-container wpas-'.$id.'-'.$type.'-container wpas-'.$type.'-container">' 358 | . 'input->getSelected(), true)) { 360 | $output .= ' checked="checked"'; 361 | } 362 | $output .= '>'; 363 | $output .= ''; 364 | $this->ctr++; 365 | return $output; 366 | } 367 | 368 | private function multi_select() { 369 | return $this->select(true); 370 | } 371 | 372 | private function text() { 373 | return $this->input('text'); 374 | } 375 | 376 | private function number() { 377 | return $this->input('number'); 378 | } 379 | 380 | private function color() { 381 | return $this->input('color'); 382 | } 383 | 384 | private function url() { 385 | return $this->input('url'); 386 | } 387 | 388 | private function email() { 389 | return $this->input('email'); 390 | } 391 | 392 | private function tel() { 393 | return $this->input('tel'); 394 | } 395 | 396 | private function date() { 397 | return $this->input('date'); 398 | } 399 | 400 | private function datetime() { 401 | return $this->input('datetime'); 402 | } 403 | 404 | private function datetime_local() { 405 | return $this->input('datetime-local'); 406 | } 407 | 408 | private function time() { 409 | return $this->input('time'); 410 | } 411 | 412 | private function week() { 413 | return $this->input('week'); 414 | } 415 | 416 | } -------------------------------------------------------------------------------- /tests/TestInputBuilder.php: -------------------------------------------------------------------------------- 1 | 'search', 17 | 'label' => 'Search', 18 | 'id' => 'search-id', 19 | 'format' => 'text', 20 | 'class' => array('testclass'), 21 | 'name' => 'my_search', 22 | 'attributes' => array('data-src' => 12345, 23 | 'data-color' => 'red', 24 | 'min' => 0, 25 | 'max' => 100), 26 | 'default' => 'something'); 27 | 28 | $input = InputBuilder::make('search_query', FieldType::search, $args); 29 | $this->assertTrue($input instanceof Input); 30 | 31 | $args = array('field_type' => 'search'); 32 | $input = InputBuilder::make('search_query', FieldType::search, $args); 33 | $this->assertTrue($input instanceof Input); 34 | $this->assertTrue($input->getFormat() == InputFormat::text); 35 | } 36 | 37 | public function testCanBuildSubmit() { 38 | $args = array('field_type' => 'submit'); 39 | $input = InputBuilder::make('submit', FieldType::submit, $args); 40 | $this->assertTrue($input instanceof Input); 41 | $this->assertTrue($input->getFormat() == InputFormat::submit); 42 | 43 | $args = array('field_type' => 'submit', 'values' => array("Go!")); 44 | $input = InputBuilder::make('submit', FieldType::submit, $args); 45 | $this->assertTrue($input instanceof Input); 46 | $values = $input->getValues(); 47 | $this->assertTrue(is_array($values) && $values[0] == "Go!"); 48 | } 49 | 50 | public function testCanBuildTaxonomy() { 51 | $args = array( 52 | 'field_type' => 'taxonomy', 53 | 'taxonomy' => 'category', 54 | 'format' => 'select' 55 | ); 56 | $input = InputBuilder::make('tax_category', FieldType::taxonomy, $args); 57 | $this->assertTrue($input instanceof Input); 58 | $this->assertFalse($input->isNested()); 59 | 60 | $args = array( 61 | 'field_type' => 'taxonomy', 62 | 'taxonomy' => 'category', 63 | 'format' => 'select', 64 | 'nested' => true 65 | ); 66 | $input = InputBuilder::make('tax_category', FieldType::taxonomy, $args); 67 | $this->assertTrue($input->isNested()); 68 | } 69 | 70 | public function testTaxonomyAutoGenTerms() { 71 | 72 | $t = array(); 73 | $t[] = $this->factory->category->create_and_get(array('name' => "Category One")); 74 | $t[] = $this->factory->category->create_and_get(array('name' => "Category Two")); 75 | $t[] = $this->factory->category->create_and_get(array('name' => "Z Category")); 76 | 77 | $args = array( 78 | 'field_type' => 'taxonomy', 79 | 'taxonomy' => 'category', 80 | 'format' => 'select', 81 | 'term_format' => 'slug' 82 | ); 83 | $input = InputBuilder::make('tax_category', FieldType::taxonomy, $args); 84 | 85 | $values = $input->getValues(); 86 | $this->assertTrue(count($values) == 4); 87 | 88 | // Test term_format = 'slug' 89 | $first_label = reset($values); 90 | $first_value = key($values); 91 | $this->assertTrue($first_label == $t[0]->name); 92 | $this->assertTrue($first_value == $t[0]->slug); 93 | 94 | // Test term_format = 'id' 95 | $args['term_format'] = 'id'; 96 | $input = InputBuilder::make('tax_category', FieldType::taxonomy, $args); 97 | $values = $input->getValues(); 98 | $first_value = key($values); 99 | $this->assertTrue(key($input->getValues()) == $t[0]->term_id); 100 | 101 | // Test term_format = 'name' 102 | $args['term_format'] = 'name'; 103 | $input = InputBuilder::make('tax_category', FieldType::taxonomy, $args); 104 | $values = $input->getValues(); 105 | $first_value = key($values); 106 | $this->assertTrue(key($input->getValues()) == $t[0]->name); 107 | 108 | // Test term_args 109 | $args['term_args'] = array('orderby' => 'name', 'order' => 'DESC'); 110 | $input = InputBuilder::make('tax_category', FieldType::taxonomy, $args); 111 | $values = $input->getValues(); 112 | $this->assertTrue(key($input->getValues()) == $t[2]->name); 113 | 114 | // Test exclude 115 | $args['exclude'] = array('category-one'); 116 | $args['term_format'] = 'slug'; 117 | $input = InputBuilder::make('tax_category', FieldType::taxonomy, $args); 118 | $values = $input->getValues(); 119 | $this->assertTrue(count($values) == 3); 120 | $this->assertFalse(isset($values[$args['exclude'][0]])); 121 | } 122 | 123 | public function testTaxonomyNestedTerms() { 124 | $t = array(); 125 | $t[] = $this->factory->category->create_and_get(array('name' => "Category One")); 126 | $t[] = $this->factory->category->create_and_get(array('name' => "Category Two", 'parent' => $t[0]->term_id)); 127 | $t[] = $this->factory->category->create_and_get(array('name' => "Z Category", 'parent' => $t[0]->term_id)); 128 | 129 | $args = array( 130 | 'field_type' => 'taxonomy', 131 | 'taxonomy' => 'category', 132 | 'format' => 'select', 133 | 'nested' => 'true', 134 | 'term_format' => 'slug' 135 | ); 136 | 137 | $input = InputBuilder::make('tax_category', FieldType::taxonomy, $args); 138 | $values = $input->getValues(); 139 | $value = reset($values); 140 | $this->assertTrue(count($value['children']) == 2); 141 | 142 | $args['format'] = 'multi-select'; 143 | $input = InputBuilder::make('tax_category', FieldType::taxonomy, $args); 144 | $values = $input->getValues(); 145 | $value = reset($values); 146 | $this->assertTrue(count($value['children']) == 2); 147 | $input->toHTML(); 148 | 149 | $args['format'] = 'radio'; 150 | $input = InputBuilder::make('tax_category', FieldType::taxonomy, $args); 151 | $values = $input->getValues(); 152 | $value = reset($values); 153 | $this->assertTrue(count($value['children']) == 2); 154 | $input->toHTML(); 155 | 156 | $args['format'] = 'checkbox'; 157 | $input = InputBuilder::make('tax_category', FieldType::taxonomy, $args); 158 | $values = $input->getValues(); 159 | $value = reset($values); 160 | $this->assertTrue(count($value['children']) == 2); 161 | $input->toHTML(); 162 | 163 | } 164 | 165 | public function testExcludeWithNestedTaxonomy() { 166 | 167 | $t = array(); 168 | $t[] = $this->factory->category->create_and_get(array('name' => "Category One")); 169 | $t[] = $this->factory->category->create_and_get(array('name' => "Category Two", 'parent' => $t[0]->term_id)); 170 | $t[] = $this->factory->category->create_and_get(array('name' => "Z Category", 'parent' => $t[0]->term_id)); 171 | 172 | $args = array( 173 | 'field_type' => 'taxonomy', 174 | 'taxonomy' => 'category', 175 | 'format' => 'select', 176 | 'nested' => 'true', 177 | 'exclude' => array('category-one'), // By excluding category-one, its two 178 | // children should also be excluded 179 | 'term_format' => 'slug' 180 | ); 181 | 182 | $input = InputBuilder::make('tax_category', FieldType::taxonomy, $args); 183 | $values = $input->getValues(); 184 | $this->assertTrue(count($values) == 1); // count = 1 due to "Uncategorized" 185 | } 186 | 187 | public function testTaxonomyWithManualTermsList() { 188 | $t = array(); 189 | $t[] = $this->factory->category->create_and_get(array('name' => "Category One")); 190 | $t[] = $this->factory->category->create_and_get(array('name' => "Category Two", 'parent' => $t[0]->term_id)); 191 | $t[] = $this->factory->category->create_and_get(array('name' => "Z Category", 'parent' => $t[0]->term_id)); 192 | $t[] = $this->factory->category->create_and_get(array('name' => "Y Category", 'parent' => $t[0]->term_id)); 193 | 194 | $args = array( 195 | 'field_type' => 'taxonomy', 196 | 'taxonomy' => 'category', 197 | 'format' => 'select', 198 | 'terms' => array($t[0]->term_id, $t[1]->term_id, $t[2]->term_id), 199 | 'term_format' => 'ID' 200 | ); 201 | 202 | $input = InputBuilder::make('tax_category', FieldType::taxonomy, $args); 203 | $values = $input->getValues(); 204 | 205 | $this->assertTrue(count($values) == 3); 206 | } 207 | 208 | 209 | public function testSetSelectedValues() { 210 | $t = array(); 211 | $t[] = $this->factory->category->create_and_get(array('name' => "Category One")); 212 | $t[] = $this->factory->category->create_and_get(array('name' => "Category Two")); 213 | $t[] = $this->factory->category->create_and_get(array('name' => "Z Category")); 214 | 215 | $request = new HttpRequest(array('tax_category' => array('category-two'))); 216 | $args = array( 217 | 'field_type' => 'taxonomy', 218 | 'taxonomy' => 'category', 219 | 'format' => 'select', 220 | 'term_format' => 'slug' 221 | ); 222 | $input = InputBuilder::make('tax_category', FieldType::taxonomy, $args, $request); 223 | 224 | // Selected value is present 225 | $selected = $input->getSelected(); 226 | $this->assertTrue(count($selected) == 1); 227 | $this->assertTrue($selected[0] == 'category-two'); 228 | 229 | // Default should be overridden by request value 230 | $args['default'] = 'category-one'; 231 | $input = InputBuilder::make('tax_category', FieldType::taxonomy, $args, 232 | $request); 233 | $selected = $input->getSelected(); 234 | $this->assertTrue(count($selected) == 1); 235 | $this->assertTrue($selected[0] == 'category-two'); 236 | 237 | // Default should be selected with empty request 238 | $request = array(); 239 | $args['default'] = 'category-one'; 240 | $input = InputBuilder::make('tax_category', FieldType::taxonomy, $args, 241 | $request); 242 | $selected = $input->getSelected(); 243 | $this->assertTrue(count($selected) == 1); 244 | $this->assertTrue($selected[0] == 'category-one'); 245 | 246 | 247 | // Check default_all 248 | $args['default_all'] = true; 249 | $args['format'] = 'multi-select'; 250 | $input = InputBuilder::make('tax_category', FieldType::taxonomy, $args, 251 | $request); 252 | $selected = $input->getSelected(); 253 | $this->assertTrue(count($selected) == 4); 254 | } 255 | 256 | public function testGenericFieldSelected() { 257 | $inputname = 'myfield'; 258 | $request = new HttpRequest(array($inputname => array('blue','green'))); 259 | 260 | $args = array( 261 | 'field_type' => 'generic', 262 | 'format' => 'checkbox', 263 | 'values' => array('red','blue','green') 264 | ); 265 | $input = InputBuilder::make($inputname, FieldType::generic, $args, 266 | $request); 267 | $this->assertTrue(count($input->getSelected()) == 2); 268 | } 269 | 270 | public function testAuthor() { 271 | $queried = array(2,4); 272 | $request = new HttpRequest(array('a' => array(2,4))); 273 | 274 | $users = array(); 275 | $users[] = $this->factory->user->create_and_get(array('display_name'=>'Sean', 'role' => 'administrator')); // id=2 276 | $users[] = $this->factory->user->create_and_get(array('display_name'=>'Dave', 'role' => 'editor')); // id=3 277 | $users[] = $this->factory->user->create_and_get(array('display_name'=>'Alex', 'role' => 'author')); // id=4 278 | $users[] = $this->factory->user->create_and_get(array('display_name'=>'Jim', 'role' => 'subscriber')); // id=5 279 | $users[] = $this->factory->user->create_and_get(array('display_name'=>'Rob', 'role' => 'contributor')); // id=6 280 | 281 | $values = array(); 282 | foreach($users as $user) { 283 | $values[] = $user->user_id; 284 | } 285 | 286 | $args = array( 287 | 'field_type' => 'author', 288 | 'format' => 'checkbox', 289 | 'values' => $values 290 | ); 291 | 292 | $input = InputBuilder::make('a', FieldType::author, $args, $request); 293 | $values = $input->getValues(); 294 | $selected = $input->getSelected(); 295 | 296 | $this->assertTrue(count($values) == 5); // 5 authors including "admin" 297 | $this->assertFalse(isset($values[5])); // User 'Jim' should not be included 298 | 299 | $this->assertTrue(count($selected) == 2); 300 | $this->assertContains(2, $selected); 301 | $this->assertContains(4,$selected); 302 | } 303 | 304 | 305 | 306 | 307 | } -------------------------------------------------------------------------------- /tests/TestFactory.php: -------------------------------------------------------------------------------- 1 | assertFalse($f->hasErrors()); 15 | 16 | } 17 | 18 | public function testFactory() { 19 | $args = array(); 20 | 21 | $args['fields'][] = array('type' => 'search', 22 | 'label' => 'Search', 23 | 'id' => 'hhh', 24 | 'format' => 'text', 25 | 'class' => 'testclass', 26 | 'name' => 'my_search', 27 | 'attributes' => array('data-src' => 12345, 'data-color' => 'red', 'min' => 0, 'max' => 100), 28 | 'default' => 'something' 29 | ); 30 | 31 | $args['fields'][] = array('type' => 'meta_key', 32 | 'label' => 'Color', 33 | 'format' => 'checkbox', 34 | 'meta_key' => 'color', 35 | 'compare' => 'LIKE', 36 | 'data_type' => 'CHAR', 37 | 'relation' => 'OR', 38 | 'values' => array( 39 | 'red' => 'Red', 40 | 'blue' => 'Blue', 41 | 'green' => 'Green', 42 | 'orange' => 'Orange', 43 | 'purple' => 'Purple', 44 | 'yellow' => 'Yellow' 45 | )); 46 | 47 | $args['fields'][] = array('type' => 'meta_key', 48 | 'label' => 'Animal', 49 | 'format' => 'checkbox', 50 | 'meta_key' => 'custom1', 51 | 'compare' => 'IN', 52 | 'data_type' => 'CHAR', 53 | 'relation' => 'OR', 54 | 'values' => array( 55 | 'Dog' => 'Dog', 56 | 'Cat' => 'Cat', 57 | 'Giraffe' => 'Giraffe', 58 | )); 59 | 60 | $args['fields'][] = array('type' => 'post_type', 61 | 'label' => 'Post Type', 62 | 'values' => array('post' => 'Post', 'page' => 'Page', 'event' => 'Event'), 63 | 'format' => 'checkbox', 64 | 'default_all' => true); 65 | 66 | $args['fields'][] = array('type' => 'post_type', 67 | 'label' => 'Post Type', 68 | 'values' => array('post' => 'Post', 'page' => 'Page', 'event' => 'Event'), 69 | 'format' => 'checkbox', 70 | 'default_all' => true); 71 | 72 | $args['fields'][] = array('type' => 'order', 73 | 'label' => 'Order', 74 | 'class' => array('orderfield', 'zzz'), 75 | 'id' => 'checkoutmyid', 76 | 'values' => array('' => '', 'ASC' => 'ASC', 'DESC' => 'DESC'), 77 | 'format' => 'select'); 78 | 79 | $args['fields'][] = array('type' => 'orderby', 80 | 'label' => 'Order By', 81 | //'exclude' => 'post_modified', 82 | 'id' => 'myorder', 83 | 'class' => 'orderclass anotherone', 84 | 'allow_null' => false, 85 | 'format' => 'radio'); 86 | 87 | $args['fields'][] = array('type' => 'submit', 88 | 'value' => 'Search', 89 | 'class' => 'submitclasss', 90 | 'attributes' => array('one' => 'five'), 91 | ); 92 | 93 | $f = new Factory($args); 94 | $this->assertFalse($f->hasErrors()); 95 | $inputs = $f->getInputs(); 96 | $this->assertTrue(count($inputs) == 8); 97 | 98 | $q = $f->buildQueryObject()->query; 99 | } 100 | 101 | public function testMetaQueryBetween() { 102 | $args = array(); 103 | $prefix = RequestVar::meta_key; 104 | $meta_key = 'price'; 105 | $compare = 'BETWEEN'; 106 | $type = 'NUMERIC'; 107 | 108 | $request = array($prefix.$meta_key.'1' => 10, $prefix.$meta_key.'2' => 25, 109 | $prefix.$meta_key.'3' => 30); 110 | 111 | $args['fields'][] = array( 112 | 'type' => 'meta_key', 113 | 'meta_key' => $meta_key, 114 | 'compare' => $compare, 115 | 'data_type' => $type, 116 | 'inputs' => array( 117 | array( 118 | 'format' => 'text', 119 | ), 120 | array( 121 | 'format' => 'text' 122 | ) 123 | ) 124 | ); 125 | 126 | $f = new Factory($args, $request); 127 | $this->assertFalse($f->hasErrors()); 128 | $q = $f->buildQueryObject()->query; 129 | $this->assertTrue(!empty($q)); 130 | $this->assertTrue(!empty($q['meta_query'])); 131 | $expected_query = '[{"key":"price","type":"NUMERIC","value":["10","25"],"compare":"BETWEEN"}]'; 132 | $this->assertTrue(json_encode($q['meta_query']) == $expected_query); 133 | 134 | } 135 | 136 | public function testMetaQueryBetweenSingleInput() { 137 | $args = array(); 138 | $prefix = RequestVar::meta_key; 139 | $meta_key = 'price'; 140 | $compare = 'BETWEEN'; 141 | $type = 'NUMERIC'; 142 | 143 | $request = array($prefix.$meta_key => '0:10'); 144 | 145 | $args['fields'][] = array( 146 | 'type' => 'meta_key', 147 | 'meta_key' => $meta_key, 148 | 'compare' => $compare, 149 | 'data_type' => $type, 150 | 'inputs' => array( 151 | array( 152 | 'format' => 'select', 153 | 'values' => array( 154 | '0:10' => '0 to 10', 155 | '11:25' => '11 to 25', 156 | '26:' => '26+' 157 | ) 158 | ), 159 | 160 | ) 161 | ); 162 | 163 | $f = new Factory($args, $request); 164 | $this->assertFalse($f->hasErrors()); 165 | $q = $f->buildQueryObject()->query; 166 | $this->assertTrue(!empty($q)); 167 | $this->assertTrue(!empty($q['meta_query'])); 168 | 169 | $expected_query = '[{"key":"price","type":"NUMERIC","value":["0","10"],"compare":"BETWEEN"}]'; 170 | $this->assertTrue(json_encode($q['meta_query']) == $expected_query); 171 | 172 | $request = array($prefix.$meta_key => '26:'); 173 | $f = new Factory($args, $request); 174 | $this->assertFalse($f->hasErrors()); 175 | $q = $f->buildQueryObject()->query; 176 | $this->assertTrue(!empty($q)); 177 | $this->assertTrue(!empty($q['meta_query'])); 178 | $expected_query = '[{"key":"price","type":"NUMERIC","value":"26","compare":">="}]'; 179 | $this->assertTrue(json_encode($q['meta_query']) == $expected_query); 180 | 181 | } 182 | 183 | public function testMetaQueryWithOneKey() 184 | { 185 | $args = array(); 186 | $meta_key = 'color'; 187 | $args['fields'][] = array( 188 | 'type' => 'meta_key', 189 | 'meta_key' => $meta_key, 190 | 'compare' => 'IN', 191 | 'relation' => 'AND', 192 | 'data_type' => 'CHAR', 193 | 'format' => 'checkbox' 194 | ); 195 | 196 | $request = array('meta_'.$meta_key => array('red','blue')); 197 | $f = new Factory($args, $request); 198 | $q = $f->buildQueryObject()->query; 199 | 200 | $this->assertFalse(empty($q['meta_query'])); 201 | $this->assertTrue(count($q['meta_query']) == 1); 202 | 203 | } 204 | 205 | public function testMetaQueryWithMultiKey() 206 | { 207 | $args = array(); 208 | $meta_key = 'color'; 209 | $meta_key2 = 'shape'; 210 | 211 | $args['fields'][] = array( 212 | 'type' => 'meta_key', 213 | 'meta_key' => $meta_key, 214 | 'compare' => 'LIKE', 215 | 'relation' => 'OR', 216 | 'data_type' => 'ARRAY', 217 | 'format' => 'checkbox' 218 | ); 219 | $args['fields'][] = array( 220 | 'type' => 'meta_key', 221 | 'meta_key' => $meta_key2, 222 | 'compare' => 'LIKE', 223 | 'relation' => 'OR', 224 | 'data_type' => 'ARRAY', 225 | 'format' => 'checkbox' 226 | ); 227 | 228 | $request = array('meta_'.$meta_key => array('red','blue'), 'meta_'.$meta_key2 => array('square','circle')); 229 | $f = new Factory($args, $request); 230 | $q = $f->buildQueryObject()->query; 231 | 232 | $this->assertFalse(empty($q['meta_query'])); 233 | $this->assertTrue(count($q['meta_query']) == 3); 234 | $this->assertTrue(count($q['meta_query'][0]) == 3); 235 | $this->assertTrue(count($q['meta_query'][1]) == 3); 236 | 237 | } 238 | 239 | public function testBadMetaKeyRelation() 240 | { 241 | $args = array(); 242 | $meta_key = 'color'; 243 | $meta_key2 = 'shape'; 244 | 245 | $args['meta_key_relation'] = "BAD"; 246 | 247 | $args['fields'][] = array( 248 | 'type' => 'meta_key', 249 | 'meta_key' => $meta_key, 250 | 'compare' => 'LIKE', 251 | 'relation' => 'OR', 252 | 'data_type' => 'ARRAY', 253 | 'format' => 'checkbox' 254 | ); 255 | $args['fields'][] = array( 256 | 'type' => 'meta_key', 257 | 'meta_key' => $meta_key2, 258 | 'compare' => 'LIKE', 259 | 'relation' => 'OR', 260 | 'data_type' => 'ARRAY', 261 | 'format' => 'checkbox' 262 | ); 263 | 264 | $request = array('meta_'.$meta_key => array('red','blue'), 'meta_'.$meta_key2 => array('square','circle')); 265 | $f = new Factory($args, $request); 266 | $q = $f->buildQueryObject()->query; 267 | 268 | $this->assertTrue($q['meta_query']['relation'] == Relation::_default); 269 | 270 | } 271 | 272 | 273 | public function testTaxQuery() 274 | { 275 | $args = array(); 276 | $tax = 'category'; 277 | $args['fields'][] = array( 278 | 'type' => 'taxonomy', 279 | 'taxonomy' => $tax, 280 | 'operator' => 'IN', 281 | 'format' => 'checkbox' 282 | ); 283 | 284 | $expected_query = '[{"taxonomy":"category","operator":"IN","field":"slug","terms":["red","blue"]}]'; 285 | 286 | $request = array(RequestVar::taxonomy . $tax => array('red','blue')); 287 | $f = new Factory($args, $request); 288 | $this->assertFalse($f->hasErrors()); 289 | 290 | $q = $f->buildQueryObject()->query; 291 | $this->assertTrue(json_encode($q['tax_query']) == $expected_query); 292 | 293 | } 294 | 295 | public function testTaxQueryMultiTax() { 296 | $args = array(); 297 | $tax = 'category'; 298 | $tax2 = 'post_tag'; 299 | $args['fields'][] = array( 300 | 'type' => 'taxonomy', 301 | 'taxonomy' => $tax, 302 | 'operator' => 'AND', 303 | 'format' => 'checkbox' 304 | ); 305 | $args['fields'][] = array( 306 | 'type' => 'taxonomy', 307 | 'taxonomy' => $tax2, 308 | 'operator' => 'IN', 309 | 'format' => 'checkbox' 310 | ); 311 | 312 | $expected_query = '{"0":{"taxonomy":"category","operator":"AND","field":"slug","terms":["red","blue"]},"1":{"taxonomy":"post_tag","operator":"IN","field":"slug","terms":["square","circle"]},"relation":"AND"}'; 313 | 314 | $request = array(RequestVar::taxonomy . $tax => array('red','blue'), 315 | RequestVar::taxonomy . $tax2 => array('square','circle')); 316 | $f = new Factory($args, $request); 317 | $this->assertFalse($f->hasErrors()); 318 | 319 | $q = $f->buildQueryObject()->query; 320 | $this->assertTrue(json_encode($q['tax_query']) == $expected_query); 321 | 322 | } 323 | 324 | public function testTaxQueryMultiInput() { 325 | $args = array(); 326 | $tax = 'category'; 327 | $args['fields'][] = array( 328 | 'type' => 'taxonomy', 329 | 'taxonomy' => $tax, 330 | 'relation' => 'AND', 331 | 'inputs' => array( 332 | array( 333 | 'format' => 'select', 334 | 'operator' => 'IN' 335 | ), 336 | array( 337 | 'format' => 'select', 338 | 'operator' => 'NOT IN' 339 | ) 340 | ) 341 | 342 | ); 343 | 344 | $expected_query = '[{"0":{"taxonomy":"category","operator":"IN","field":"slug","terms":"red"},"1":{"taxonomy":"category","operator":"NOT IN","field":"slug","terms":["blue","green"]},"relation":"AND"}]'; 345 | 346 | $request = array(RequestVar::taxonomy . $tax . '1' => 'red', 347 | RequestVar::taxonomy . $tax . '2' => array('blue','green')); 348 | $f = new Factory($args, $request); 349 | $this->assertFalse($f->hasErrors()); 350 | 351 | $q = $f->buildQueryObject()->query; 352 | 353 | $this->assertTrue(json_encode($q['tax_query']) == $expected_query); 354 | } 355 | 356 | public function testTaxQueryMultiInputEmptyInput() { 357 | $args = array(); 358 | $tax = 'category'; 359 | $args['fields'][] = array( 360 | 'type' => 'taxonomy', 361 | 'taxonomy' => $tax, 362 | 'relation' => 'AND', 363 | 'inputs' => array( 364 | array( 365 | 'format' => 'select', 366 | 'operator' => 'IN' 367 | ), 368 | array( 369 | 'format' => 'select', 370 | 'operator' => 'NOT IN' 371 | ) 372 | ) 373 | ); 374 | 375 | $expected_query = '[{"taxonomy":"category","operator":"IN","field":"slug","terms":"red"}]'; 376 | 377 | $request = array(RequestVar::taxonomy . $tax . '1' => 'red', 378 | RequestVar::taxonomy . $tax . '2' => array()); 379 | $f = new Factory($args, $request); 380 | $this->assertFalse($f->hasErrors()); 381 | 382 | $q = $f->buildQueryObject()->query; 383 | $this->assertTrue(json_encode($q['tax_query']) == $expected_query); 384 | } 385 | 386 | public function testFactoryComplexRequest() { 387 | $args = array(); 388 | 389 | $args['fields'][] = array('type' => 'search', 390 | 'label' => 'Search', 391 | //'id' => 'hhh', 392 | 'format' => 'text', 393 | 'class' => 'testclass', 394 | 'name' => 'my_search', 395 | 'attributes' => array('data-src' => 12345, 'data-color' => 'red', 'min' => 0, 'max' => 100), 396 | 'default' => 'something' 397 | ); 398 | 399 | $args['fields'][] = array('type' => 'post_type', 400 | 'label' => 'Post Type', 401 | 'values' => array('post' => 'Post', 'page' => 'Page', 'event' => 'Event'), 402 | 'format' => 'checkbox', 403 | 'default_all' => true); 404 | 405 | $args['fields'][] = array('type' => 'order', 406 | 'label' => 'Order', 407 | 'id' => 'checkoutmyid', 408 | 'values' => array('' => '', 'ASC' => 'ASC', 'DESC' => 'DESC'), 409 | 'format' => 'select'); 410 | 411 | $args['fields'][] = array('type' => 'orderby', 412 | 'label' => 'Order By', 413 | 'id' => 'myorder', 414 | 'allow_null' => false, 415 | 'format' => 'radio'); 416 | 417 | $args['fields'][] = array('type' => 'posts_per_page', 418 | 'label' => 'Order By', 419 | 'id' => 'myorder', 420 | 'allow_null' => false, 421 | 'format' => 'radio'); 422 | 423 | $args['fields'][] = array('type' => 'submit', 424 | 'value' => 'Search', 425 | 'attributes' => array('one' => 'five'), 426 | ); 427 | 428 | $request = array('order'=>'ASC', 'ptype'=>array('post','page'), 'search_query' => 'testing', 'posts_per_page' => 12); 429 | $expected_query = '{"post_type":["post","page"],"order":"ASC","posts_per_page":"12","s":"testing","paged":1}'; 430 | $e = '{"post_type":["post","page"],"order":"ASC","posts_per_page":12,"s":"testing","paged":1}'; 431 | 432 | $f = new Factory($args, $request); 433 | $this->assertFalse($f->hasErrors()); 434 | $q = $f->buildQueryObject()->query; 435 | $this->assertTrue(json_encode($q) == $expected_query); 436 | 437 | } 438 | 439 | public function testFactoryDateQuery() { 440 | $args = array(); 441 | $args['fields'][] = array( 442 | 'type' => 'date', 443 | 'format' => 'select', 444 | 'date_type' => 'month' 445 | ); 446 | 447 | $expected_query = '{"year":"2014","month":"01"}'; 448 | 449 | $request = array(RequestVar::date . 'm' => '2014-01'); 450 | $f = new Factory($args, $request); 451 | $this->assertFalse($f->hasErrors()); 452 | 453 | $q = $f->buildQueryObject()->query; 454 | $this->assertFalse(empty($q['date_query'])); 455 | $this->assertTrue(json_encode($q['date_query']) == $expected_query); 456 | 457 | } 458 | 459 | public function testOrderbyValues() { 460 | $args = array(); 461 | $args['fields'][] = array( 462 | 'type' => 'orderby', 463 | 'format' => 'radio', 464 | 'default' => 'event_date', 465 | 'title' => 'Order by', 466 | 'orderby_values' => 467 | array( 468 | 'event_date' => array('label' => 'Date', 469 | 'meta_key' => true, 470 | 'orderby' => 'meta_value'), 471 | 'event_price' => array('label' => 'Date', 472 | 'meta_key' => true, 473 | 'orderby' => 'meta_value_num'), 474 | 'title' => array('label' => 'Title'), 475 | )); 476 | 477 | // Test meta_value_num 478 | $request = array(RequestVar::orderby => 'event_price'); 479 | $f = new Factory($args, $request); 480 | $this->assertFalse($f->hasErrors()); 481 | $q = $f->buildQueryObject()->query; 482 | $this->assertTrue($q['orderby'] == 'meta_value_num'); 483 | $this->assertTrue($q['meta_key'] == 'event_price'); 484 | 485 | // Test meta_value 486 | $request = array(RequestVar::orderby => 'event_date'); 487 | $f = new Factory($args, $request); 488 | $this->assertFalse($f->hasErrors()); 489 | $q = $f->buildQueryObject()->query; 490 | $this->assertTrue($q['orderby'] == 'meta_value'); 491 | $this->assertTrue($q['meta_key'] == 'event_date'); 492 | 493 | // Test regular orderby 494 | $request = array(RequestVar::orderby => 'title'); 495 | $f = new Factory($args, $request); 496 | $this->assertFalse($f->hasErrors()); 497 | $q = $f->buildQueryObject()->query; 498 | $this->assertTrue($q['orderby'] == 'title'); 499 | $this->assertTrue(!isset($q['meta_key'])); 500 | 501 | } 502 | 503 | public function testGenericFieldIdApplies() { 504 | $args = array(); 505 | $args['fields'][] = array( 506 | 'type' => 'generic', 507 | 'format' => 'select', 508 | 'id' => 'bleep' 509 | ); 510 | $f = new Factory($args); 511 | $inputs = $f->getForm()->getInputs(); 512 | $input = $inputs[0]; 513 | $this->assertTrue($input->getId() == 'bleep'); 514 | } 515 | 516 | 517 | 518 | } --------------------------------------------------------------------------------