From d494cbd80b53c6dd9956d07140ca779cf85d4518 Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Fri, 21 Jul 2017 10:13:10 +0300 Subject: [PATCH 01/31] add .gitignore --- .gitignore | 209 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..deea66e --- /dev/null +++ b/.gitignore @@ -0,0 +1,209 @@ +tests/vendor +/nbproject/private/ +nbproject + +# + +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + + +# + + +# + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + + + +# + + +# + +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + +# + + +# + +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +modules.order +Module.symvers +Mkfile.old +dkms.conf + + +# + + +# + +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + + +# + +tmp +lib +vendor From 48f30f4dc354e6be521a3dca63f854d20c661b14 Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Fri, 21 Jul 2017 10:13:28 +0300 Subject: [PATCH 02/31] add .mixtapefile --- .mixtapefile | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .mixtapefile diff --git a/.mixtapefile b/.mixtapefile new file mode 100644 index 0000000..8e2631b --- /dev/null +++ b/.mixtapefile @@ -0,0 +1,3 @@ +sha=bc384c9df8b92a347b39c0aacc1d97b17f06a566 +prefix=Zoninator_REST +destination=lib/zoninator_rest From 838ec1392de94fe6f28b751e7afb4869c7f31b21 Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Fri, 21 Jul 2017 11:59:29 +0300 Subject: [PATCH 03/31] initial rest api commit --- includes/class-zoninator-api-controller.php | 600 ++++++++++++++++++ .../class-zoninator-api-filter-search.php | 78 +++ .../class-zoninator-api-schema-converter.php | 69 ++ includes/class-zoninator-api.php | 46 ++ scripts/build_mixtape.sh | 114 ++++ zoninator.php | 154 ++++- 6 files changed, 1041 insertions(+), 20 deletions(-) create mode 100644 includes/class-zoninator-api-controller.php create mode 100644 includes/class-zoninator-api-filter-search.php create mode 100644 includes/class-zoninator-api-schema-converter.php create mode 100644 includes/class-zoninator-api.php create mode 100755 scripts/build_mixtape.sh diff --git a/includes/class-zoninator-api-controller.php b/includes/class-zoninator-api-controller.php new file mode 100644 index 0000000..e2db02f --- /dev/null +++ b/includes/class-zoninator-api-controller.php @@ -0,0 +1,600 @@ +[\d]+)'; + const ZONE_ITEM_POSTS_URL_REGEX = '/zones/(?P[\d]+)/posts'; + const ZONE_ITEM_POSTS_POST_REGEX = '/zones/(?P[\d]+)/posts/(?P\d+)'; + + const INVALID_ZONE_ID = 'invalid-zone-id'; + const INVALID_POST_ID = 'invalid-post-id'; + const ZONE_ID_POST_ID_REQUIRED = 'zone-id-post-id-required'; + const ZONE_ID_POST_IDS_REQUIRED = 'zone-id-post-ids-required'; + const ZONE_ID_REQUIRED = 'zone-id-required'; + const ZONE_FEED_ERROR = 'zone-feed-error'; + const TERM_REQUIRED = 'term-required'; + const PERMISSION_DENIED = 'permission-denied'; + const ZONE_NOT_FOUND = 'zone-not-found'; + const POST_NOT_FOUND = 'post-not-found'; + /** + * Instance + * + * @var Zoninator + */ + private $instance; + /** + * Key Value Translation array + * + * @var array + */ + private $translations; + + /** + * Zoninator_Api_Controller constructor. + * + * @param string $base Base. + * @param Zoninator $instance Instance. + */ + function __construct( $instance ) { + $this->instance = $instance; + $this->base = '/'; + } + + /** + * Set up this controller + */ + function setup() { +// $this->add_route( 'zones' ) +// ->add_action( $this->action( 'index', 'get_zones' ) ) +// ->add_action( $this->action( 'create', 'create_zone' ) ); + $this->translations = array( + self::ZONE_NOT_FOUND => __( 'Zone not found', 'zoninator' ), + self::INVALID_POST_ID => __( 'Invalid post id', 'zoninator' ), + self::INVALID_ZONE_ID => __( 'Zone not found', 'zoninator' ), + self::ZONE_ID_POST_ID_REQUIRED => __( 'post id and zone id required', 'zoninator' ), + ); + + $this->add_route( 'zones/(?P[\d]+)' ) + ->add_action( $this->action( 'index', 'get_zone_posts' ) + ->permissions( 'get_zone_posts_permissions_check' ) + ->args( '_get_zone_id_param' ) + ); + + $this->add_route( 'zones/(?P[\d]+)/posts' ) + ->add_action( $this->action( 'create', 'add_post_to_zone' ) + ->permissions( 'add_post_to_zone_permissions_check' ) + ->args( '_get_zone_post_rest_route_params' ) + ) + ->add_action( $this->action( 'update', 'add_post_to_zone' ) + ->permissions( 'add_post_to_zone_permissions_check' ) + ->args( '_get_zone_post_rest_route_params' ) + ); + + $this->add_route( 'zones/(?P[\d]+)/posts/(?P\d+)' ) + ->add_action( $this->action( 'delete', 'remove_post_from_zone' ) + ->permissions( 'remove_post_from_zone_permissions_check' ) + ->args( '_get_zone_post_rest_route_params' ) ); + + $this->add_route( 'zones/(?P[\d]+)/posts/order' ) + ->add_action( $this->action( 'update', 'reorder_posts' ) + ->permissions( 'update_zone_permissions_check' ) + ->args( '_get_zone_id_param' ) ); + + $this->add_route( 'zones/(?P[\d]+)/lock' ) + ->add_action( $this->action( 'update', 'zone_update_lock' ) + ->permissions( 'update_zone_permissions_check' ) + ->args( '_get_zone_id_param' ) ); + + $this->add_route( 'posts/search' ) + ->add_action( $this->action( 'index', 'search_posts' ) + ->args( '_params_for_search_posts' ) ); + + $this->add_route( 'posts/recent' ) + ->add_action( $this->action( 'index', 'get_recent_posts' ) + ->args( '_params_for_get_recent_posts' ) ); + } + + /** + * Add a post to zone + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|WP_REST_Response + */ + function add_post_to_zone( $request ) { + $zone_id = $this->_get_param( $request, 'zone_id', 0, 'absint' ); + $post_id = $this->_get_param( $request, 'post_id', 0, 'absint' ); + + $post = get_post( $post_id ); + + if ( ! $post ) { + $data = array( + 'message' => $this->translations[ self::INVALID_POST_ID ], + ); + return $this->bad_request( $data ); + } + + $zone = $this->instance->get_zone( $zone_id ); + + if ( ! $zone ) { + + return $this->not_found( $this->translations[ self::INVALID_POST_ID ] ); + } + + $result = $this->instance->add_zone_posts( $zone_id, $post, true ); + + if ( is_wp_error( $result ) ) { + return $this->respond( $result, 500 ); + } + + $content = $this->instance->get_admin_zone_post( $post, $zone ); + + $response_data = array( + 'zone_id' => $zone_id, + 'content' => $content, + ); + + return WP_REST_Server::CREATABLE === $request->get_method() ? + $this->created( $response_data ) : + $this->ok( $response_data ); + } + + /** + * Delete one item from the collection. + * + * @param WP_REST_Request $request The Request. + * @return WP_Error|WP_REST_Response + */ + function remove_post_from_zone( $request ) { + $zone_id = $this->_get_param( $request, 'zone_id', 0, 'absint' ); + $post_id = $this->_get_param( $request, 'post_id', 0, 'absint' ); + + if ( empty( $zone_id ) ) { + $data = array( + 'message' => $this->translations[ self::ZONE_ID_POST_ID_REQUIRED ], + ); + return $this->bad_request( $data ); + } + + if ( false === $this->instance->get_zone( $zone_id ) ) { + return $this->not_found( $this->translations[ self::ZONE_NOT_FOUND ] ); + } + + if ( empty( $post_id ) ) { + $data = array( + 'message' => $this->translations[ self::ZONE_ID_POST_ID_REQUIRED ], + ); + return $this->bad_request( $data ); + } + + $post = get_post( $post_id ); + if ( empty( $post ) ) { + $data = array( + 'message' => $this->translations[ self::INVALID_POST_ID ], + ); + return $this->bad_request( $data ); + } + + $result = $this->instance->remove_zone_posts( $zone_id, $post_id ); + + if ( is_wp_error( $result ) ) { + return $this->respond( $result, 500 ); + } + + $result = array( + 'zone_id' => $zone_id, + 'post_id' => $post_id, + 'content' => '', + 'status' => 200, + ); + return $this->ok( $result ); + } + + /** + * Reorder posts for zone. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|WP_REST_Response + */ + function reorder_posts( $request ) { + $zone_id = $this->_get_param( $request, 'zone_id', 0, 'absint' ); + $post_ids = (array) $this->_get_param( $request, 'posts', array(), 'absint' ); + + if ( ! $zone_id ) { + return $this->_bad_request( self::ZONE_ID_POST_IDS_REQUIRED, __( 'post ids and zone id required', 'zoninator') ); + } + + if ( false === $this->instance->get_zone( $zone_id ) ) { + return $this->not_found( $this->translations[ self::ZONE_NOT_FOUND ] ); + } + + if ( empty( $post_ids ) ) { + return $this->_bad_request( self::ZONE_ID_POST_IDS_REQUIRED, __( 'post ids and zone id required', 'zoninator' ) ); + } + + $result = $this->instance->add_zone_posts( $zone_id, $post_ids, false ); + + if (is_wp_error($result)) { + $status = 0; + $http_status = 500; + $content = $result->get_error_message(); + } else { + $status = 1; + $http_status = 200; + $content = ''; + } + + return new WP_REST_Response( array( + 'zone_id' => $zone_id, + 'post_ids' => $post_ids, + 'content' => $content, + 'status' => $status, + ), $http_status ); + } + + /** + * Update the zone's lock + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|WP_REST_Response + */ + function zone_update_lock( $request ) { + $zone_id = $this->_get_param( $request, 'zone_id', 0, 'absint' ); + + if (! $zone_id ) { + return $this->_bad_request(self::ZONE_ID_REQUIRED, __('zone id required', 'zoninator')); + } + + if ( ! $this->instance->is_zone_locked( $zone_id ) ) { + $this->instance->lock_zone( $zone_id ); + return new WP_REST_Response(array( + 'zone_id' => $zone_id, + 'status' => 1, + ), 200); + } + + return new WP_REST_Response(array( + 'zone_id' => $zone_id, + 'status' => 0), 400); + } + + /** + * Search posts for "term" + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|WP_REST_Response + */ + function search_posts( $request ) { + $search_filter_definition = $this->environment()->model( 'Zoninator_Api_Filter_Search' ); + $search_filter = $search_filter_definition->new_from_array( $request->get_params() ); + + $validation_error = $search_filter->validate(); + if ( is_wp_error( $validation_error ) ) { + return $this->respond( $validation_error, 400 ); + } + + $filter_cat = $search_filter->get( 'cat' ); + $filter_date = $search_filter->get( 'date' ); + + $post_types = $this->instance->get_supported_post_types(); + $limit = $this->_get_param( $request, 'limit', $this->instance->posts_per_page ); + + if ( 0 >= $limit ) { + $limit = $this->instance->posts_per_page; + } + + $exclude = (array)$search_filter->get( 'exclude' ); + + $args = apply_filters('zoninator_search_args', array( + 's' => $search_filter->get( 'term' ), + 'post__not_in' => $exclude, + 'posts_per_page' => $limit, + 'post_type' => $post_types, + 'post_status' => array('publish', 'future'), + 'order' => 'DESC', + 'orderby' => 'post_date', + 'suppress_filters' => true, + )); + + if ( $this->instance->_validate_category_filter( $filter_cat ) ) { + $args['cat'] = $filter_cat; + } + + if ( $this->instance->_validate_date_filter( $filter_date ) ) { + $filter_date_parts = explode( '-', $filter_date ); + $args['year'] = $filter_date_parts[0]; + $args['monthnum'] = $filter_date_parts[1]; + $args['day'] = $filter_date_parts[2]; + } + + $query = new WP_Query($args); + + $stripped_posts = array(); + + if ( $query->have_posts() ) { + foreach ( $query->posts as $post ) { + $stripped_posts[] = apply_filters( 'zoninator_search_results_post', array( + 'title' => !empty( $post->post_title ) ? $post->post_title : __( '(no title)', 'zoninator' ), + 'post_id' => $post->ID, + 'date' => get_the_time( get_option( 'date_format' ), $post ), + 'post_type' => $post->post_type, + 'post_status' => $post->post_status, + ), $post ); + } + } + + return new WP_REST_Response( $stripped_posts, 200 ); + } + + /** + * Get zone posts + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_zone_posts( $request ) { + $zone_id = $this->_get_param( $request, 'zone_id', 0, 'absint' ); + + if ( empty( $zone_id ) || false === $this->instance->get_zone( $zone_id ) ) { + return $this->not_found( $this->translations[ self::INVALID_ZONE_ID ] ); + } + + $results = $this->instance->get_zone_feed( $zone_id ); + + if ( is_wp_error( $results ) ) { + return $this->_bad_request( self::ZONE_FEED_ERROR, $results->get_error_message() ); + } + + return new WP_REST_Response( $results, 200 ); + } + + /** + * Get recent posts, excluding the ones that are already part of the zone provided + * Recent posts can be filtered by category and date + * + * @param WP_REST_Request $request + * @return WP_Error|WP_REST_Response + */ + public function get_recent_posts( $request ) { + $cat = $this->_get_param( $request, 'cat', '', 'absint' ); + $date = $this->_get_param( $request, 'date', '', 'striptags' ); + $zone_id = $this->_get_param( $request, 'zone_id', 0, 'absint' ); + + $limit = $this->instance->posts_per_page; + $post_types = $this->instance->get_supported_post_types(); + $zone_posts = $this->instance->get_zone_posts( $zone_id ); + $zone_post_ids = wp_list_pluck( $zone_posts, 'ID' ); + + $http_status = 200; + + if ( is_wp_error( $zone_posts ) ) { + $status = 0; + $content = $zone_posts->get_error_message(); + $http_status = 500; + } else { + $args = apply_filters( 'zoninator_recent_posts_args', array( + 'posts_per_page' => $limit, + 'order' => 'DESC', + 'orderby' => 'post_date', + 'post_type' => $post_types, + 'ignore_sticky_posts' => true, + 'post_status' => array( 'publish', 'future' ), + 'post__not_in' => $zone_post_ids, + ) ); + + if ( $this->instance->_validate_category_filter( $cat ) ) { + $args['cat'] = $cat; + } + + if ( $this->instance->_validate_date_filter( $date ) ) { + $filter_date_parts = explode( '-', $date ); + $args['year'] = $filter_date_parts[0]; + $args['monthnum'] = $filter_date_parts[1]; + $args['day'] = $filter_date_parts[2]; + } + + $content = ''; + $recent_posts = get_posts( $args ); + foreach ( $recent_posts as $post ) { + $content .= sprintf('', $post->ID, get_the_title($post->ID) . ' (' . $post->post_status . ')'); + } + + wp_reset_postdata(); + $status = 1; + } + + if ( ! $content ) { + $empty_label = __( 'No results found', 'zoninator' ); + } elseif ( $cat ) { + $empty_label = sprintf(__('Choose post from %s', 'zoninator'), get_the_category_by_ID($cat)); + } else { + $empty_label = __( 'Choose a post', 'zoninator' ); + } + + $content = '' . $content; + + $response = new WP_REST_Response( array( + 'zone_id' => $zone_id, + 'content' => $content, + 'status' => $status ) ); + $response->set_status( $http_status ); + return $response; + } + + /** + * Check if a given request has access to get zone posts. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|bool + */ + public function get_zone_posts_permissions_check( $request ) { + return true; + } + + /** + * Check if a given request has access to remove a post from zone. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|bool + */ + public function remove_post_from_zone_permissions_check( $request ) { + $zone_id = $this->_get_param( $request, 'zone_id', 0, 'absint' ); + return $this->_permissions_check( 'update', $zone_id ); + } + + /** + * Check if a given request has access to add a post in a zone. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|bool + */ + public function add_post_to_zone_permissions_check( $request ) { + $zone_id = $this->_get_param( $request, 'zone_id', 0, 'absint' ); + return $this->_permissions_check( 'insert', $zone_id ); + } + + /** + * Check if a given request has access to update a zone. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|bool + */ + public function update_zone_permissions_check( $request ) { + $zone_id = $this->_get_param( $request, 'zone_id', 0, 'absint' ); + return $this->_permissions_check( 'update', $zone_id ); + } + + public function is_numeric( $item ) { + // see https://github.com/WP-API/WP-API/issues/1520 on why we do not use is_numeric directly + return is_numeric( $item ); + } + + public function strip_slashes( $item ) { + // see https://github.com/WP-API/WP-API/issues/1520 on why we do not use stripslashes directly + return stripslashes( $item ); + } + + /** + * @param WP_REST_Request $object + * @param $var + * @param string $default + * @param string $sanitize_callback + * @return array|mixed|null|string + */ + private function _get_param( $object, $var, $default = '', $sanitize_callback = '' ) { + $value = $object->get_param( $var ); + $value = ( $value !== null ) ? $value : $default; + + + if ( is_callable( $sanitize_callback ) ) { + $value = ( is_array( $value ) ) ? array_map( $sanitize_callback, $value ) : call_user_func( $sanitize_callback, $value ); + } + + return $value; + } + + public function _get_zone_id_param() { + return array( + 'zone_id' => array( + 'type' => 'integer', + 'validate_callback' => array( $this, 'is_numeric' ), + 'sanitize_callback' => 'absint', + 'required' => true + ) + ); + } + + public function _get_zone_post_rest_route_params() { + $zone_params = $this->_get_zone_id_param(); + return array_merge(array( + 'post_id' => array( + 'type' => 'integer', + 'validate_callback' => array( $this, 'is_numeric' ), + 'required' => true + ) + ), $zone_params); + } + + public function _params_for_get_recent_posts() + { + $zone_params = $this->_get_zone_id_param(); + return array_merge(array( + 'cat' => array( + 'description' => __( 'only recent posts from this category id', 'zoninator' ), + 'type' => 'integer', + 'validate_callback' => array( $this, 'is_numeric' ), + 'sanitize_callback' => 'absint', + 'default' => 0, + 'required' => false + ), + 'date' => array( + 'description' => __( 'only get posts after this date (format YYYY-mm-dd)', 'zoninator' ), + 'type' => 'string', + 'sanitize_callback' => array( $this, 'strip_slashes' ), + 'default' => '', + 'required' => false + ) + ), $zone_params); + } + + public function _params_for_search_posts() { + $search_filter = $this->environment()->model( 'Zoninator_Api_Filter_Search' ); + $schema_converter = new Zoninator_Api_Schema_Converter(); + return $schema_converter->as_args( $search_filter ); +// return array( +// 'term' => array( +// 'description' => __( 'search term', 'zoninator' ), +// 'type' => 'string', +// 'sanitize_callback' => array( $this, 'strip_slashes' ), +// 'default' => '', +// 'required' => true +// ), +// 'cat' => array( +// 'description' => __( 'filter by category', 'zoninator' ), +// 'type' => 'integer', +// 'validate_callback' => array( $this, 'is_numeric' ), +// 'sanitize_callback' => 'absint', +// 'default' => 0, +// 'required' => false +// ), +// 'date' => array( +// 'description' => __( 'only get posts after this date (format YYYY-mm-dd)', 'zoninator' ), +// 'type' => 'string', +// 'sanitize_callback' => array( $this, 'strip_slashes' ), +// 'default' => '', +// 'required' => false +// ), +// 'limit' => array( +// 'description' => __( 'limit results', 'zoninator' ), +// 'type' => 'integer', +// 'sanitize_callback' => 'absint', +// 'default' => $this->instance->posts_per_page, +// 'required' => false +// ), +// 'exclude' => array( +// 'description' => __( 'post_ids to exclude', 'zoninator' ), +// 'required' => false +// ) +// ); + } + + private function _bad_request($code, $message) { + return new WP_Error( $code, $message, array( 'status' => 400 ) ); + } + + /** + * @param $zone_id + * @return bool|WP_Error + */ + private function _permissions_check($action, $zone_id = null ) { + if ( ! $this->instance->check( $action, $zone_id ) ) { + return new WP_Error( self::PERMISSION_DENIED, __('Sorry, you\'re not supposed to do that...', 'zoninator' ) ); + } + return true; + } +} \ No newline at end of file diff --git a/includes/class-zoninator-api-filter-search.php b/includes/class-zoninator-api-filter-search.php new file mode 100644 index 0000000..f3c8ac8 --- /dev/null +++ b/includes/class-zoninator-api-filter-search.php @@ -0,0 +1,78 @@ +field( 'term', __( 'search term', 'zoninator' ) ) + ->with_type( $env->type( 'string' ) ) + ->with_before_set( array( $this, 'strip_slashes' ) ) + ->with_required( true ) + ->with_default( '' ), + $env->field( 'cat', __( 'filter by category', 'zoninator' ) ) + ->with_type( $env->type( 'uint' ) ) + ->with_validations( array( $this, 'is_numeric' ) ) + ->with_default( 0 ), + $env->field( 'date', __( 'only get posts after this date (format YYYY-mm-dd)', 'zoninator' ) ) + ->with_type( $env->type( 'string' ) ) + ->with_before_set( array( $this, 'date_before_set' ) ) + ->with_default( '' ), + $env->field( 'limit', __( 'limit results', 'zoninator' ) ) + ->with_type( $env->type( 'uint' ) ) + ->with_before_set( array( $this, 'strip_slashes' ) ) + ->with_default( Zoninator()->posts_per_page ), + $env->field( 'exclude', __( 'post_ids to exclude', 'zoninator' ) ) + ->with_type( $env->type( 'array:uint' ) ), + ); + } + + /** + * Is Numeric + * + * @param mixed $item The item. + * @return bool + */ + public function is_numeric( $model, $item ) { + // see https://github.com/WP-API/WP-API/issues/1520 on why we do not use is_numeric directly + return is_numeric( $item ); + } + + /** + * Strip slashes + * + * @param mixed $item Item. + * @return string + */ + public function strip_slashes( $model, $item ) { + // see https://github.com/WP-API/WP-API/issues/1520 on why we do not use stripslashes directly + return stripslashes( $item ); + } + + public function strip_tags( $model, $item ) { + return strip_tags( $item ); + } + + function date_before_set( $model, $item ) { + return $this->strip_tags( $model, $this->strip_slashes( $model, $item ) ); + } +} + diff --git a/includes/class-zoninator-api-schema-converter.php b/includes/class-zoninator-api-schema-converter.php new file mode 100644 index 0000000..5b1b3c2 --- /dev/null +++ b/includes/class-zoninator-api-schema-converter.php @@ -0,0 +1,69 @@ +get_field_declarations(); + $properties = array(); + $required = array(); + foreach ( $fields as $field_declaration ) { + /** + * Our declaration + * + * @var Zoninator_REST_Field_Declaration $field_declaration + */ + $properties[ $field_declaration->get_data_transfer_name() ] = $field_declaration->as_item_schema_property(); + if ( $field_declaration->is_required() ) { + $required[] = $field_declaration->get_data_transfer_name(); + } + } + $schema = array( + '$schema' => 'http://json-schema.org/schema#', + 'title' => $model_definition->get_name(), + 'type' => 'object', + 'properties' => (array) apply_filters( 'rest_api_schema_properties', $properties, $model_definition ), + ); + + if ( ! empty( $required ) ) { + $schema['required'] = $required; + } + + return $schema; + } + + /** + * As Schema + * + * @param Zoninator_REST_Model_Definition $model_definition Def. + * @return array + */ + public function as_args( $model_definition ) { + $fields = $model_definition->get_field_declarations(); + $result = array(); + foreach ( $fields as $field_declaration ) { + $type_schema = $field_declaration->get_type()->schema(); + /** + * Our declaration + * + * @var Zoninator_REST_Field_Declaration $field_declaration + */ + $arg = array( + 'description' => $field_declaration->get_description(), + 'type' => $type_schema['type'], + 'required' => $field_declaration->is_required(), + ); + + if ( ! $field_declaration->is_required() ) { + $arg['default'] = $field_declaration->get_default_value(); + } + + $result[ $field_declaration->get_data_transfer_name() ] = $arg; + } + return $result; + } +} \ No newline at end of file diff --git a/includes/class-zoninator-api.php b/includes/class-zoninator-api.php new file mode 100644 index 0000000..7a20a32 --- /dev/null +++ b/includes/class-zoninator-api.php @@ -0,0 +1,46 @@ +instance = $instance; + add_action( 'rest_api_init', array( $this, 'rest_api' ) ); + } + + /** + * Rest Api. + */ + function rest_api() { + include_once ZONINATOR_PATH . '/lib/zoninator_rest/class-zoninator-rest-bootstrap.php'; + $this->bootstrap = Zoninator_REST_Bootstrap::create()->load(); + include_once 'class-zoninator-api-schema-converter.php'; + include_once 'class-zoninator-api-filter-search.php'; + include_once 'class-zoninator-api-controller.php'; + $env = $this->bootstrap->environment(); + + $env->define_model( 'Zoninator_Api_Filter_Search' ); + + $env->rest_api( 'zoninator/v1' ) + ->add_endpoint( new Zoninator_Api_Controller( $this->instance ) ); + $env->start(); + return $this; + } + +} \ No newline at end of file diff --git a/scripts/build_mixtape.sh b/scripts/build_mixtape.sh new file mode 100755 index 0000000..c1d6047 --- /dev/null +++ b/scripts/build_mixtape.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash + +set -e; + +SCRIPT_ROOT=`pwd`; +MIXTAPE_TEMP_PATH="$SCRIPT_ROOT/tmp/mt"; +MIXTAPE_REPO="https://github.com/Automattic/mixtape/"; +MIXTAPEFILE_NAME=".mixtapefile"; +MIXTAPE_PATH="${MIXTAPE_PATH-$MIXTAPE_TEMP_PATH}"; + +# Declare all our reusable functions + +show_usage() { + echo "Builds mixtape for development and plugin deployment"; + echo "Note: Requires git"; + echo ""; + echo " ./scripts/build_mixtape.sh"; + echo ""; + echo "Assumes $MIXTAPEFILE_NAME is present at project root, generates a stub otherwise"; +}; + +expect_directory() { + if [ ! -d "$1" ]; then + echo "Not a directory: $1. Exiting" >&2; + exit 1; + fi +}; + +# Check we have git +command -v git >/dev/null 2>&1 || { + echo "No Git found. Exiting." >&2; + show_usage; + exit 1; +}; + +if [ "$MIXTAPE_PATH" == "$MIXTAPE_TEMP_PATH" ]; then + if [ ! -d "$MIXTAPE_PATH" ]; then + mkdir -p "$MIXTAPE_PATH"; + git clone "$MIXTAPE_REPO" "$MIXTAPE_PATH" || { + echo "Error cloning mixtape repo: $MIXTAPE_REPO" >&2; + exit 1; + } + cd "$MIXTAPE_PATH" && git checkout master >/dev/null 2>&1; + if [ "$?" -ne 0 ]; then + echo "Can't run git checkout command on $MIXTAPE_PATH" >&2; + exit 1; + fi + fi + cd "$MIXTAPE_PATH" && git fetch 2>&1; +fi + +cd "$SCRIPT_ROOT"; + +expect_directory "$MIXTAPE_PATH"; + +if [ ! -f "$MIXTAPEFILE_NAME" ]; then + echo "No $MIXTAPEFILE_NAME found. Generating one (using sha from Mixtape HEAD)"; + + echo "sha=$(cd $MIXTAPE_PATH && git rev-parse HEAD)" >> "$MIXTAPEFILE_NAME"; + echo "prefix=YOUR_PREFIX" >> "$MIXTAPEFILE_NAME"; + echo "destination=your/destination" >> "$MIXTAPEFILE_NAME"; + + echo "$MIXTAPEFILE_NAME Generated:"; + echo ""; + cat "$MIXTAPEFILE_NAME"; + echo "Amend it with your prefix, sha and destination and rerun this."; + exit; +fi + +cd "$SCRIPT_ROOT"; + +mt_current_sha="$(cat "$MIXTAPEFILE_NAME" | grep -o 'sha=[^"]*' | sed 's/sha=//')"; +mt_current_prefix="$(cat "$MIXTAPEFILE_NAME" | grep -o 'prefix=[^"]*' | sed 's/prefix=//')"; +mt_current_destination="$(pwd)/$(cat "$MIXTAPEFILE_NAME" | grep -o 'destination=[^"]*' | sed 's/destination=//')"; + +echo "============= Building Mixtape ============="; +echo ""; +echo "SHA = $mt_current_sha"; +echo "PREFIX = $mt_current_prefix"; +echo "DESTINATION = $mt_current_destination"; +echo ""; + +expect_directory "$mt_current_destination"; + +cd $MIXTAPE_PATH; +mt_repo_current_sha="$(git rev-parse HEAD)"; + +if [ "$mt_repo_current_sha" != "$mt_current_sha" ]; then + echo "Dir"; + git checkout "$mt_current_sha" 2>&1; + if [ $? -ne 0 ]; then + echo "Git checkout error" >&2; + exit 1; + fi +fi + +git diff-index --quiet --cached HEAD >/dev/null 2>&1; + +if [ $? -ne 0 ]; then + echo "Repository (at $MIXTAPE_PATH) is dirty. Please commit or stash the changes. Exiting." >&2; + exit 1; +fi + +echo "Running project script from $MIXTAPE_PATH" +sh "$MIXTAPE_PATH/scripts/new_project.sh" "$mt_current_prefix" "$mt_current_destination"; + +if [ $? -ne 0 ]; then + echo "Something went wrong with the file generation, Exiting" >&2; + git checkout "$mt_repo_current_sha" >/dev/null 2>&1; + exit 1; +else + echo "Generation done!"; + git checkout "$mt_repo_current_sha" >/dev/null 2>&1; +fi diff --git a/zoninator.php b/zoninator.php index c26bc1d..7083e39 100644 --- a/zoninator.php +++ b/zoninator.php @@ -56,6 +56,10 @@ class Zoninator ); var $zone_messages = null; var $posts_per_page = 10; + /** + * @var Zoninator_Api + */ + public $rest_api = null; function __construct() { add_action( 'init', array( $this, 'init' ), 99 ); // init later after other post types have been registered @@ -70,9 +74,21 @@ function __construct() { add_action( 'split_shared_term', array( $this, 'split_shared_term' ), 10, 4 ); + $this->maybe_add_rest_api(); + $this->default_post_types = array( 'post' ); } + public function maybe_add_rest_api() { + global $wp_version; + if ( version_compare( $wp_version, '4.7', '<' ) ) { + return false; + } + + include_once 'includes/class-zoninator-api.php'; + $this->rest_api = new Zoninator_Api( $this ); + } + function add_zone_feed() { add_rewrite_tag( '%' . $this->zone_taxonomy . '%', '([^&]+)' ); add_rewrite_rule( '^zones/([^/]+)/feed.json/?$', 'index.php?' . $this->zone_taxonomy . '=$matches[1]', 'top' ); @@ -965,6 +981,12 @@ function delete_zone( $zone ) { return new WP_Error( 'invalid-zone', __( 'Sorry, that zone doesn\'t exist.', 'zoninator' ) ); } + /** + * @param $zone + * @param $posts + * @param bool $append + * @return bool|WP_Error + */ function add_zone_posts( $zone, $posts, $append = false ) { $zone = $this->get_zone( $zone ); $meta_key = $this->get_zone_meta_key( $zone ); @@ -995,8 +1017,15 @@ function add_zone_posts( $zone, $posts, $append = false ) { clean_term_cache( $this->get_zone_id( $zone ), $this->zone_taxonomy ); // flush cache for our zone term and related APC caches do_action( 'zoninator_add_zone_posts', $posts, $zone ); + + return true; } + /** + * @param $zone + * @param null $posts + * @return bool|WP_Error + */ function remove_zone_posts( $zone, $posts = null ) { $zone = $this->get_zone( $zone ); $meta_key = $this->get_zone_meta_key( $zone ); @@ -1347,41 +1376,32 @@ function _fill_zone_details( $zone ) { } function do_zoninator_feeds() { - - global $wp_query; - $query_var = get_query_var( $this->zone_taxonomy ); if ( ! empty( $query_var ) ) { $zone_slug = get_query_var( $this->zone_taxonomy ); - $zone_id = $this->get_zone( $zone_slug ); - - if ( empty( $zone_id ) ) { - $this->send_user_error( __( 'Invalid zone supplied', 'zoninator' ) ); + $results = $this->get_zone_feed( $zone_slug ); + if ( is_wp_error( $results ) ) { + $this->send_user_error( $results->get_error_message() ); } - - $results = $this->get_zone_posts( $zone_id, apply_filters( 'zoninator_json_feed_fields', array(), $zone_slug ) ); - - if ( empty( $results ) ) { - $this->send_user_error( __( 'No zone posts found', 'zoninator' ) ); - } - - $filtered_results = $this->filter_zone_feed_fields( $results ); - - $this->json_return( apply_filters( 'zoninator_json_feed_results', $filtered_results, $zone_slug ), false ); + $this->json_return( $results, false ); } return; } - private function filter_zone_feed_fields( $results ) { + private function filter_zone_feed_fields( $results ) { + $filtered_results = array(); $whitelisted_fields = array( 'ID', 'post_date', 'post_title', 'post_content', 'post_excerpt', 'post_status', 'guid' ); $filtered_results = array(); $i = 0; foreach ( $results as $result ) { + if ( ! isset( $filtered_results[ $i ] ) ) { + $filtered_results[$i] = new stdClass(); + } foreach( $whitelisted_fields as $field ) { if ( ! isset ( $filtered_results[$i] ) ) { $filtered_results[$i] = new stdClass; @@ -1571,9 +1591,103 @@ function _get_get_var( $var, $default = '', $sanitize_callback = '' ) { function _get_post_var( $var, $default = '', $sanitize_callback = '' ) { return $this->_get_value_or_default( $var, $_POST, $default, $sanitize_callback ); } + + public function get_admin_zone_post($post, $zone) { + return apply_filters('zoninator_zone_post_columns', array( + 'post_id' => $post->ID, + 'position' => array( + 'current_position' => intval( $this->get_post_order( $post->ID, $zone ) ), + 'change_position_message' => esc_attr__( 'Click and drag to change the position of this item.', 'zoninator' ), + 'key' => 'position' + ), + 'info' => array( + 'key' => 'info', + 'post' => array( + 'post_title' => esc_html( $post->post_title ), + 'post_status' => esc_html( $post->post_status ) + ), + 'action_link_data' => array( + array( + 'action' => 'edit', + 'anchor' => get_edit_post_link( $post->ID ), + 'title' => __( 'Opens in new window', 'zoninator' ), + 'text' => __( 'Edit', 'zoninator' ), + 'target' => "_blank" + ), + array( + 'action' => 'delete', + 'anchor' => '#', + 'title' => '', + 'text' => __( 'Remove', 'zoninator' ) + ), + array( + 'action' => 'view', + 'anchor' => get_permalink( $post->ID ), + 'title' => __( 'Opens in new window', 'zoninator' ), + 'text' => __( 'View', 'zoninator' ), + 'target' => "_blank" + ) + ) + ) + ), $post, $zone); + } + + public function get_admin_zone_posts( $zone_or_id ) { + $zone = $this->get_zone( $zone_or_id ); + $posts = $this->get_zone_posts( $zone ); + $admin_zone_posts = array(); + + foreach ( $posts as $post ) { + $admin_zone_posts[] = $this->get_admin_zone_post( $post, $zone ); + } + + return $admin_zone_posts; + } + + /** + * @param $zone_slug_or_id + * @return array|WP_Error + */ + function get_zone_feed( $zone_slug_or_id ) { + $zone_id = $this->get_zone( $zone_slug_or_id ); + + if ( empty( $zone_id ) ) { + return new WP_Error( 'invalid-zone-supplied', __( 'Invalid zone supplied', 'zoninator' ) ); + } + + $results = $this->get_zone_posts( $zone_id, apply_filters( 'zoninator_json_feed_fields', array(), $zone_slug_or_id ) ); + + if ( empty( $results ) ) { + return new WP_Error( 'no-zone-posts-found', __( 'No zone posts found', 'zoninator' ) ); + } + + $filtered_results = $this->filter_zone_feed_fields( $results ); + + return apply_filters( 'zoninator_json_feed_results', $filtered_results, $zone_slug_or_id ); + } + + public function check( $action = '', $zone_id = null ) { + // TODO: should check if zone locked + if ( 'insert' == $action ) { + return $this->_current_user_can_add_zones(); + } + + if ( 'update' == $action || 'delete' == $action ) { + return $this->_current_user_can_edit_zones( $zone_id ); + } + + return $this->_current_user_can_manage_zones(); + } +} + +function Zoninator() { + global $zoninator; + if ( ! isset( $zoninator ) || null === $zoninator ) { + $zoninator = new Zoninator; + } + return $zoninator; } -global $zoninator; -$zoninator = new Zoninator; +Zoninator(); endif; From 63b027001c85c8f1fc8178694b8f7ffe2cb7c2e2 Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Fri, 21 Jul 2017 12:09:10 +0300 Subject: [PATCH 04/31] add tests --- .travis.yml | 23 + composer.json | 18 + composer.lock | 1798 +++++++++++++++++ phpunit.xml | 39 + tests/README.md | 8 + tests/acceptance/test_zoninator_admin.js | 276 +++ tests/bin/install-wp-tests.sh | 120 ++ tests/bootstrap.php | 19 + tests/run_acceptance_tests.sh | 3 + tests/setup_test_env.sh | 32 + .../class-zoninator-api-controller-test.php | 532 +++++ 11 files changed, 2868 insertions(+) create mode 100644 .travis.yml create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 phpunit.xml create mode 100644 tests/README.md create mode 100644 tests/acceptance/test_zoninator_admin.js create mode 100755 tests/bin/install-wp-tests.sh create mode 100755 tests/bootstrap.php create mode 100755 tests/run_acceptance_tests.sh create mode 100755 tests/setup_test_env.sh create mode 100644 tests/unit/class-zoninator-api-controller-test.php diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..7903648 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,23 @@ +language: php + +notifications: + email: + on_success: never + on_failure: change + +php: + - 5.3 + - 5.6 + +env: + - WP_VERSION=latest WP_MULTISITE=0 + +matrix: + include: + - php: 5.3 + env: WP_VERSION=latest WP_MULTISITE=1 + +before_script: + - bash tests/bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION + +script: phpunit diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..273412f --- /dev/null +++ b/composer.json @@ -0,0 +1,18 @@ +{ + "name": "automattic/zoninator", + "description": "Zone Editor", + "type": "test suite", + "require": { + "phpunit/phpunit": "4.8", + "wp-cli/wp-cli": "^0.21.1", + "psy/psysh": "^0.6.1" + }, + "license": "GPL v2", + "authors": [ + { + "name": "Automattic", + "email": "info@automattic.com" + } + ], + "minimum-stability": "dev" +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..e7fd786 --- /dev/null +++ b/composer.lock @@ -0,0 +1,1798 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "hash": "f673840b09aa5fad1d303dde4ec6a298", + "content-hash": "2f924054373b5ee2e4b5f8c99d9acc75", + "packages": [ + { + "name": "composer/semver", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "d0e1ccc6d44ab318b758d709e19176037da6b1ba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/d0e1ccc6d44ab318b758d709e19176037da6b1ba", + "reference": "d0e1ccc6d44ab318b758d709e19176037da6b1ba", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.5", + "phpunit/phpunit-mock-objects": "~2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.1-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com" + }, + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "time": "2015-09-21 09:42:36" + }, + { + "name": "dnoegel/php-xdg-base-dir", + "version": "0.1", + "source": { + "type": "git", + "url": "https://github.com/dnoegel/php-xdg-base-dir.git", + "reference": "265b8593498b997dc2d31e75b89f053b5cc9621a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dnoegel/php-xdg-base-dir/zipball/265b8593498b997dc2d31e75b89f053b5cc9621a", + "reference": "265b8593498b997dc2d31e75b89f053b5cc9621a", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "@stable" + }, + "type": "project", + "autoload": { + "psr-4": { + "XdgBaseDir\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "implementation of xdg base directory specification for php", + "time": "2014-10-24 07:27:01" + }, + { + "name": "doctrine/instantiator", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", + "shasum": "" + }, + "require": { + "php": ">=5.3,<8.0-DEV" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "ext-pdo": "*", + "ext-phar": "*", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://github.com/doctrine/instantiator", + "keywords": [ + "constructor", + "instantiate" + ], + "time": "2015-06-14 21:17:01" + }, + { + "name": "jakub-onderka/php-console-color", + "version": "0.1", + "source": { + "type": "git", + "url": "https://github.com/JakubOnderka/PHP-Console-Color.git", + "reference": "e0b393dacf7703fc36a4efc3df1435485197e6c1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/JakubOnderka/PHP-Console-Color/zipball/e0b393dacf7703fc36a4efc3df1435485197e6c1", + "reference": "e0b393dacf7703fc36a4efc3df1435485197e6c1", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "jakub-onderka/php-code-style": "1.0", + "jakub-onderka/php-parallel-lint": "0.*", + "jakub-onderka/php-var-dump-check": "0.*", + "phpunit/phpunit": "3.7.*", + "squizlabs/php_codesniffer": "1.*" + }, + "type": "library", + "autoload": { + "psr-0": { + "JakubOnderka\\PhpConsoleColor": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Jakub Onderka", + "email": "jakub.onderka@gmail.com", + "homepage": "http://www.acci.cz" + } + ], + "time": "2014-04-08 15:00:19" + }, + { + "name": "jakub-onderka/php-console-highlighter", + "version": "v0.3.2", + "source": { + "type": "git", + "url": "https://github.com/JakubOnderka/PHP-Console-Highlighter.git", + "reference": "7daa75df45242c8d5b75a22c00a201e7954e4fb5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/JakubOnderka/PHP-Console-Highlighter/zipball/7daa75df45242c8d5b75a22c00a201e7954e4fb5", + "reference": "7daa75df45242c8d5b75a22c00a201e7954e4fb5", + "shasum": "" + }, + "require": { + "jakub-onderka/php-console-color": "~0.1", + "php": ">=5.3.0" + }, + "require-dev": { + "jakub-onderka/php-code-style": "~1.0", + "jakub-onderka/php-parallel-lint": "~0.5", + "jakub-onderka/php-var-dump-check": "~0.1", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~1.5" + }, + "type": "library", + "autoload": { + "psr-0": { + "JakubOnderka\\PhpConsoleHighlighter": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jakub Onderka", + "email": "acci@acci.cz", + "homepage": "http://www.acci.cz/" + } + ], + "time": "2015-04-20 18:58:01" + }, + { + "name": "mustache/mustache", + "version": "v2.9.0", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/mustache.php.git", + "reference": "c745b01956caf27d26b55a72a90aff56bc169cd6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/mustache.php/zipball/c745b01956caf27d26b55a72a90aff56bc169cd6", + "reference": "c745b01956caf27d26b55a72a90aff56bc169cd6", + "shasum": "" + }, + "require": { + "php": ">=5.2.4" + }, + "require-dev": { + "fabpot/php-cs-fixer": "~1.6", + "phpunit/phpunit": "~3.7|~4.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "Mustache": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info", + "homepage": "http://justinhileman.com" + } + ], + "description": "A Mustache implementation in PHP.", + "homepage": "https://github.com/bobthecow/mustache.php", + "keywords": [ + "mustache", + "templating" + ], + "time": "2015-08-15 19:23:13" + }, + { + "name": "nb/oxymel", + "version": "v0.1.0", + "source": { + "type": "git", + "url": "https://github.com/nb/oxymel.git", + "reference": "cbe626ef55d5c4cc9b5e6e3904b395861ea76e3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nb/oxymel/zipball/cbe626ef55d5c4cc9b5e6e3904b395861ea76e3c", + "reference": "cbe626ef55d5c4cc9b5e6e3904b395861ea76e3c", + "shasum": "" + }, + "require": { + "php": ">=5.2.4" + }, + "type": "library", + "autoload": { + "psr-0": { + "Oxymel": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nikolay Bachiyski", + "email": "nb@nikolay.bg", + "homepage": "http://extrapolate.me/" + } + ], + "description": "A sweet XML builder", + "homepage": "https://github.com/nb/oxymel", + "keywords": [ + "xml" + ], + "time": "2013-02-24 15:01:54" + }, + { + "name": "nikic/php-parser", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "719ca71d4ac80e1985dcd91dd8ec5a47db58ad80" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/719ca71d4ac80e1985dcd91dd8ec5a47db58ad80", + "reference": "719ca71d4ac80e1985dcd91dd8ec5a47db58ad80", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.4" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "time": "2015-12-07 11:12:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d68dbdc53dc358a816f00b300704702b2eaff7b8", + "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "suggest": { + "dflydev/markdown": "~1.0", + "erusev/parsedown": "~1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "phpDocumentor": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "mike.vanriel@naenius.com" + } + ], + "time": "2015-02-03 12:10:50" + }, + { + "name": "phpspec/prophecy", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "e55e3e32a870bd4f05425fa4f717b52bd40e5659" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/e55e3e32a870bd4f05425fa4f717b52bd40e5659", + "reference": "e55e3e32a870bd4f05425fa4f717b52bd40e5659", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": "^5.3|^7.0", + "phpdocumentor/reflection-docblock": "~2.0", + "sebastian/comparator": "~1.1", + "sebastian/recursion-context": "~1.0" + }, + "require-dev": { + "phpspec/phpspec": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5.x-dev" + } + }, + "autoload": { + "psr-0": { + "Prophecy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "time": "2015-12-28 13:26:33" + }, + { + "name": "phpunit/php-code-coverage", + "version": "2.2.x-dev", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/eabf68b476ac7d0f73793aada060f1c1a9bf8979", + "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "phpunit/php-file-iterator": "~1.3", + "phpunit/php-text-template": "~1.2", + "phpunit/php-token-stream": "~1.3", + "sebastian/environment": "^1.3.2", + "sebastian/version": "~1.0" + }, + "require-dev": { + "ext-xdebug": ">=2.1.4", + "phpunit/phpunit": "~4" + }, + "suggest": { + "ext-dom": "*", + "ext-xdebug": ">=2.2.1", + "ext-xmlwriter": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "time": "2015-10-06 15:47:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6150bf2c35d3fc379e50c7602b75caceaa39dbf0", + "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "time": "2015-06-21 13:08:43" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "time": "2015-06-21 13:50:34" + }, + { + "name": "phpunit/php-timer", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3e82f4e9fc92665fafd9157568e4dcb01d014e5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3e82f4e9fc92665fafd9157568e4dcb01d014e5b", + "reference": "3e82f4e9fc92665fafd9157568e4dcb01d014e5b", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "time": "2015-06-21 08:01:12" + }, + { + "name": "phpunit/php-token-stream", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "cab6c6fefee93d7b7c3a01292a0fe0884ea66644" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/cab6c6fefee93d7b7c3a01292a0fe0884ea66644", + "reference": "cab6c6fefee93d7b7c3a01292a0fe0884ea66644", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "time": "2015-09-23 14:46:55" + }, + { + "name": "phpunit/phpunit", + "version": "4.8.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "283111a903eb9225aedb95e846bef876e006a688" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/283111a903eb9225aedb95e846bef876e006a688", + "reference": "283111a903eb9225aedb95e846bef876e006a688", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "php": ">=5.3.3", + "phpspec/prophecy": "^1.3.1", + "phpunit/php-code-coverage": "~2.1", + "phpunit/php-file-iterator": "~1.4", + "phpunit/php-text-template": "~1.2", + "phpunit/php-timer": ">=1.0.6", + "phpunit/phpunit-mock-objects": "~2.3", + "sebastian/comparator": "~1.1", + "sebastian/diff": "~1.2", + "sebastian/environment": "~1.3", + "sebastian/exporter": "~1.2", + "sebastian/global-state": "~1.0", + "sebastian/version": "~1.0", + "symfony/yaml": "~2.1|~3.0" + }, + "suggest": { + "phpunit/php-invoker": "~1.1" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.8.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "time": "2015-08-07 03:57:43" + }, + { + "name": "phpunit/phpunit-mock-objects", + "version": "2.3.x-dev", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", + "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/ac8e7a3db35738d56ee9a76e78a4e03d97628983", + "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": ">=5.3.3", + "phpunit/php-text-template": "~1.2", + "sebastian/exporter": "~1.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "suggest": { + "ext-soap": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Mock Object library for PHPUnit", + "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", + "keywords": [ + "mock", + "xunit" + ], + "time": "2015-10-02 06:51:40" + }, + { + "name": "psy/psysh", + "version": "v0.6.1", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/psysh.git", + "reference": "0f04df0b23663799a8941fae13cd8e6299bde3ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/0f04df0b23663799a8941fae13cd8e6299bde3ed", + "reference": "0f04df0b23663799a8941fae13cd8e6299bde3ed", + "shasum": "" + }, + "require": { + "dnoegel/php-xdg-base-dir": "0.1", + "jakub-onderka/php-console-highlighter": "0.3.*", + "nikic/php-parser": "^1.2.1|~2.0", + "php": ">=5.3.9", + "symfony/console": "~2.3.10|^2.4.2|~3.0", + "symfony/var-dumper": "~2.7|~3.0" + }, + "require-dev": { + "fabpot/php-cs-fixer": "~1.5", + "phpunit/phpunit": "~3.7|~4.0|~5.0", + "squizlabs/php_codesniffer": "~2.0", + "symfony/finder": "~2.1|~3.0" + }, + "suggest": { + "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", + "ext-pdo-sqlite": "The doc command requires SQLite to work.", + "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well.", + "ext-readline": "Enables support for arrow-key history navigation, and showing and manipulating command history." + }, + "bin": [ + "bin/psysh" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-develop": "0.7.x-dev" + } + }, + "autoload": { + "files": [ + "src/Psy/functions.php" + ], + "psr-4": { + "Psy\\": "src/Psy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info", + "homepage": "http://justinhileman.com" + } + ], + "description": "An interactive shell for modern PHP.", + "homepage": "http://psysh.org", + "keywords": [ + "REPL", + "console", + "interactive", + "shell" + ], + "time": "2015-11-12 16:18:56" + }, + { + "name": "ramsey/array_column", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/ramsey/array_column.git", + "reference": "f8e52eb28e67eb50e613b451dd916abcf783c1db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/array_column/zipball/f8e52eb28e67eb50e613b451dd916abcf783c1db", + "reference": "f8e52eb28e67eb50e613b451dd916abcf783c1db", + "shasum": "" + }, + "require-dev": { + "jakub-onderka/php-parallel-lint": "0.8.*", + "phpunit/phpunit": "~4.5", + "satooshi/php-coveralls": "0.6.*", + "squizlabs/php_codesniffer": "~2.2" + }, + "type": "library", + "autoload": { + "files": [ + "src/array_column.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "homepage": "http://benramsey.com" + } + ], + "description": "Provides functionality for array_column() to projects using PHP earlier than version 5.5.", + "homepage": "https://github.com/ramsey/array_column", + "keywords": [ + "array", + "array_column", + "column" + ], + "time": "2015-03-20 22:07:39" + }, + { + "name": "rmccue/requests", + "version": "v1.6.1", + "source": { + "type": "git", + "url": "https://github.com/rmccue/Requests.git", + "reference": "6aac485666c2955077d77b796bbdd25f0013a4ea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rmccue/Requests/zipball/6aac485666c2955077d77b796bbdd25f0013a4ea", + "reference": "6aac485666c2955077d77b796bbdd25f0013a4ea", + "shasum": "" + }, + "require": { + "php": ">=5.2" + }, + "require-dev": { + "satooshi/php-coveralls": "dev-master" + }, + "type": "library", + "autoload": { + "psr-0": { + "Requests": "library/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Ryan McCue", + "homepage": "http://ryanmccue.info" + } + ], + "description": "A HTTP library written in PHP, for human beings.", + "homepage": "http://github.com/rmccue/Requests", + "keywords": [ + "curl", + "fsockopen", + "http", + "idna", + "ipv6", + "iri", + "sockets" + ], + "time": "2014-05-18 04:59:02" + }, + { + "name": "sebastian/comparator", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "937efb279bd37a375bcadf584dec0726f84dbf22" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/937efb279bd37a375bcadf584dec0726f84dbf22", + "reference": "937efb279bd37a375bcadf584dec0726f84dbf22", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/diff": "~1.2", + "sebastian/exporter": "~1.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "http://www.github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "time": "2015-07-26 15:48:44" + }, + { + "name": "sebastian/diff", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e", + "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff" + ], + "time": "2015-12-08 07:14:41" + }, + { + "name": "sebastian/environment", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "6e7133793a8e5a5714a551a8324337374be209df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/6e7133793a8e5a5714a551a8324337374be209df", + "reference": "6e7133793a8e5a5714a551a8324337374be209df", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "time": "2015-12-02 08:37:27" + }, + { + "name": "sebastian/exporter", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "f88f8936517d54ae6d589166810877fb2015d0a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/f88f8936517d54ae6d589166810877fb2015d0a2", + "reference": "f88f8936517d54ae6d589166810877fb2015d0a2", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/recursion-context": "~1.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "time": "2015-08-09 04:23:41" + }, + { + "name": "sebastian/global-state", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "time": "2015-10-12 03:26:01" + }, + { + "name": "sebastian/recursion-context", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "913401df809e99e4f47b27cdd781f4a258d58791" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/913401df809e99e4f47b27cdd781f4a258d58791", + "reference": "913401df809e99e4f47b27cdd781f4a258d58791", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "time": "2015-11-11 19:50:13" + }, + { + "name": "sebastian/version", + "version": "1.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", + "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", + "shasum": "" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "time": "2015-06-21 13:59:46" + }, + { + "name": "symfony/console", + "version": "2.8.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "4e83844dead78e6e90b8b7dff862fa232fa228ad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/4e83844dead78e6e90b8b7dff862fa232fa228ad", + "reference": "4e83844dead78e6e90b8b7dff862fa232fa228ad", + "shasum": "" + }, + "require": { + "php": ">=5.3.9", + "symfony/polyfill-mbstring": "~1.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/event-dispatcher": "~2.1|~3.0.0", + "symfony/process": "~2.1|~3.0.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/process": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Console Component", + "homepage": "https://symfony.com", + "time": "2015-12-28 13:12:56" + }, + { + "name": "symfony/finder", + "version": "2.8.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "dd41ae57f4f737be271d944a0cc5f5f21203a7c6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/dd41ae57f4f737be271d944a0cc5f5f21203a7c6", + "reference": "dd41ae57f4f737be271d944a0cc5f5f21203a7c6", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Finder Component", + "homepage": "https://symfony.com", + "time": "2015-12-05 11:09:21" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "49ff736bd5d41f45240cec77b44967d76e0c3d25" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/49ff736bd5d41f45240cec77b44967d76e0c3d25", + "reference": "49ff736bd5d41f45240cec77b44967d76e0c3d25", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "time": "2015-11-20 09:19:13" + }, + { + "name": "symfony/var-dumper", + "version": "2.8.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "f943f29ae69c42511a2d85adee9d13d835b5c803" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/f943f29ae69c42511a2d85adee9d13d835b5c803", + "reference": "f943f29ae69c42511a2d85adee9d13d835b5c803", + "shasum": "" + }, + "require": { + "php": ">=5.3.9", + "symfony/polyfill-mbstring": "~1.0" + }, + "require-dev": { + "twig/twig": "~1.20|~2.0" + }, + "suggest": { + "ext-symfony_debug": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony mechanism for exploring and dumping PHP variables", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "time": "2015-12-05 11:09:21" + }, + { + "name": "symfony/yaml", + "version": "2.8.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "cee3fdda8184add20a13eb666295dfd72e8031bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/cee3fdda8184add20a13eb666295dfd72e8031bd", + "reference": "cee3fdda8184add20a13eb666295dfd72e8031bd", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Yaml Component", + "homepage": "https://symfony.com", + "time": "2015-12-28 13:12:56" + }, + { + "name": "wp-cli/php-cli-tools", + "version": "v0.10.5", + "source": { + "type": "git", + "url": "https://github.com/wp-cli/php-cli-tools.git", + "reference": "037a010441a5c220cd1df26cdc9b20ad73408356" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wp-cli/php-cli-tools/zipball/037a010441a5c220cd1df26cdc9b20ad73408356", + "reference": "037a010441a5c220cd1df26cdc9b20ad73408356", + "shasum": "" + }, + "require": { + "php": ">= 5.3.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "cli": "lib/" + }, + "files": [ + "lib/cli/cli.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "James Logsdon", + "email": "jlogsdon@php.net", + "role": "Developer" + }, + { + "name": "Daniel Bachhuber", + "email": "daniel@handbuilt.co", + "role": "Maintainer" + } + ], + "description": "Console utilities for PHP", + "homepage": "http://github.com/wp-cli/php-cli-tools", + "keywords": [ + "cli", + "console" + ], + "time": "2015-08-10 12:46:19" + }, + { + "name": "wp-cli/wp-cli", + "version": "v0.21.1", + "source": { + "type": "git", + "url": "https://github.com/wp-cli/wp-cli.git", + "reference": "21da8b4ebf7f37f9ae55b45b037214f154a1b2ab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wp-cli/wp-cli/zipball/21da8b4ebf7f37f9ae55b45b037214f154a1b2ab", + "reference": "21da8b4ebf7f37f9ae55b45b037214f154a1b2ab", + "shasum": "" + }, + "require": { + "composer/semver": "1.0.0", + "mustache/mustache": "~2.4", + "nb/oxymel": "0.1.0", + "php": ">=5.3.2", + "ramsey/array_column": "~1.1", + "rmccue/requests": "~1.6", + "symfony/finder": "~2.3", + "wp-cli/php-cli-tools": "0.10.5" + }, + "require-dev": { + "behat/behat": "2.5.*", + "phpunit/phpunit": "3.7.*" + }, + "suggest": { + "psy/psysh": "Enhanced `wp shell` functionality" + }, + "bin": [ + "bin/wp.bat", + "bin/wp" + ], + "type": "library", + "autoload": { + "psr-0": { + "WP_CLI": "php" + }, + "files": [ + "php/Spyc.php" + ], + "classmap": [ + "php/export" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A command line interface for WordPress", + "homepage": "http://wp-cli.org", + "keywords": [ + "cli", + "wordpress" + ], + "time": "2015-11-23 13:50:12" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..ab85e39 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + ./tests/ + ./tests/unit-testing-classes + ./tests/bin + + + + + ./apigen/ + ./tests/ + ./tests/unit-testing-classes + ./tmp/ + + + + + ./src + + + + diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..f6ac458 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,8 @@ +### Running Unit Tests + +PHPUnit test setup only tested under Chassis. + +```bash +./setup_test_env.sh +./vendor/bin/phpunit +``` \ No newline at end of file diff --git a/tests/acceptance/test_zoninator_admin.js b/tests/acceptance/test_zoninator_admin.js new file mode 100644 index 0000000..3ac1699 --- /dev/null +++ b/tests/acceptance/test_zoninator_admin.js @@ -0,0 +1,276 @@ +/* + +### Zoninator Admin Acceptance tests + +Requires Linux/OSX and working CasperJS and WordPress installations. + +Links + +- CasperJS: http://casperjs.org/ +- Local WordPress setup: https://github.com/Chassis/Chassis + or https://roots.io/trellis/ or https://github.com/Varying-Vagrant-Vagrants/VVV + +### Usage + +expects that the following environment variables are set: + + export WP_INT_BASEURL='http://example.com' + export WP_INT_ADMIN_ROOT='/wp/wp-admin' + export WP_INT_ADMIN_USERNAME='user' + export WP_INT_ADMIN_PASS='pass' + export WP_INT_DEBUG='' + +### Running the tests + + casperjs test tests/acceptance/test_zoninator_admin.js + +for additional debug data run `WP_INT_DEBUG=1 casperjs test tests/acceptance/test_zoninator_admin.js` +*/ + +var system = require('system'); + +var WAIT_TIMEOUT = 5000; + +var baseUrl = system.env.WP_INT_BASEURL || 'http://vagrant.local'; +var adminRoot = system.env.WP_INT_ADMIN_ROOT || '/wp/wp-admin'; +var adminUserName = system.env.WP_INT_ADMIN_USERNAME || 'admin'; +var adminPassword = system.env.WP_INT_ADMIN_PASS || 'password'; +var debug = (system.env.WP_INT_DEBUG) ? true : false; + +var adminUrl = baseUrl + adminRoot, + adminAddNewPostUrl = adminUrl + '/post-new.php', + adminPostIndex = adminUrl + '/edit.php', + adminZoninatorPage = adminUrl + '/admin.php?page=zoninator', + adminActivePluginIndex = adminUrl + '/plugins.php?plugin_status=active', + integrationTestZoneTitle = 'Integration Test Zone'; + + +if (debug) { + casper.options.verbose = true; + casper.options.logLevel = 'debug'; +} + + +casper.test.begin('Zoninator Acceptance Tests', { + test: function (test) { + "use strict"; + + casper.zoneDeleted = function (casper) { + var zones = this.fetchText('.zone-tab'); + return (zones.indexOf(integrationTestZoneTitle) === -1); + }; + + casper.assertPostCanBeRemovedFromZone = function (postId) { + casper.then(function () { + this.echo('When I press Remove on zone post with id ' + postId); + var selector = '#zone-post-' + postId; + casper.waitForSelector(selector, function () { + casper.thenClick(selector + ' .delete', function () { + }); + + casper.then(function () { + this.echo('And I wait for the transition effect'); + this.wait(1000); + }); + + casper.then(function () { + test.assertNotExists(selector, 'Then the post is removed from the Zone'); + }); + }); + }); + }; + + casper.assertCanAddPostToZone = function (postId) { + casper.waitForSelector('#zone-post-latest', function () { + this.echo('When I select the post with id ' + postId); + this.fillSelectors('.zone-search-wrapper', { + '#zone-post-latest' : postId + }, false); + }); + + casper.then(function () { + casper.waitForSelector('#zone-post-' + postId, function () { + test.assertExists('#zone-post-' + postId, 'Then Post with id ' + postId + ' is added to the Zone'); + }); + }); + }; + + casper.setFilter('page.confirm', function(message) { + this.echo('And the Page displayed a message that will be confirmed: ' + message); + return true; + }); + + casper.on('resource.requested', function (request) { + // Wait on All REST API AJAX requests + // This should be called within the test context and + // we should not be using PhatomJS onResourceRequested + // as it causes some weirdness + if (request.url.indexOf('zoninator/v1') >= 0) { + var formattedRequest = [request.method, request.url].join(' '); + this.echo('And I Wait until XHR Request completes: ' + formattedRequest, 'info'); + casper.waitForResource(request.url, function(){ + this.echo('And the XHR request is completed: ' + formattedRequest, 'info'); + this.wait(250); + }, function(){ + this.echo('But the XHR request times out: ' + formattedRequest, 'info'); + }, WAIT_TIMEOUT); + } + }); + + casper.on('resource.received', function (response){ + // Capture responses for all REST API AJAX requests + if (response.url.indexOf('zoninator/v1') >= 0) { + var parts = ['id:', response.id, 'status:', response.status, 'url:', response.url] + this.log('XHR Response received: ' + parts.join(' '), 'info'); + } + }); + + casper.start(adminUrl, function() { + this.echo('When I visit ' + adminUrl); + test.assertTitle("WordPress Site › Log In", "I am at the login page"); + test.assertExists('form[name="loginform"]', "And I can find a login form"); + this.echo('When I fill the login form and submit'); + this.fill('form[name="loginform"]', { + log: adminUserName, + pwd: adminPassword + }, true); + }); + + casper.then(function() { + test.assertTitle('Dashboard ‹ WordPress Site — WordPress', + 'I am at WordPress Admin Dashboard'); + }); + + casper.thenOpen(adminPostIndex); + + casper.waitForSelector('tr.status-publish', function () { + test.assertExists('tr.status-publish', + 'The site has published posts'); + }); + + casper.thenOpen(adminActivePluginIndex); + + casper.waitForSelector('.plugin-title', function () { + test.assertSelectorHasText('.plugin-title', + 'Zone Manager (Zoninator)', + 'Zoninator Plugin is active'); + }); + + casper.thenOpen(adminZoninatorPage); + + casper.then(function () { + test.assertTitle('Zoninator ‹ WordPress Site — WordPress', + 'Zoninator Page Exists'); + }); + + casper.then(function () { + if (!this.zoneDeleted()) { + this.echo('Test Zone Found... cleaning up'); + casper.then(function () { + this.clickLabel(integrationTestZoneTitle, 'a'); + }); + + casper.then(function () { + this.clickLabel('Delete','a'); + }); + } + }); + + casper.waitForSelector('form[id="zone-info"]', function () { + test.assertExists('form[id="zone-info"]', "zone-info form is found"); + this.fill('form[id="zone-info"]', { + name: integrationTestZoneTitle, + description: 'Zone used by integration tests and can be safely deleted' + }, true); + }); + + casper.waitForSelector('.zone-tab', function () { + test.assertSelectorHasText('.zone-tab', + integrationTestZoneTitle, + 'Integration Test Zone created'); + }); + + casper.then(function () { + this.clickLabel(integrationTestZoneTitle, 'span', + 'I Click on the zone Titled ' + integrationTestZoneTitle); + }); + + casper.then(function () { + test.assertUrlMatch(/zoninator&action=edit&zone_id=/, 'Then Edit Zone Page is Rendered'); + test.assertSelectorHasText('#zone-description', + 'Zone used by integration tests and can be safely deleted', + 'And The Zone contains the expected description'); + }); + + casper.waitForSelector('form[id="zone-info"]', function () { + test.assertExists('form[id="zone-info"]', + 'zone-info form is found and ready for Editing'); + this.fill('form[id="zone-info"]', { + description: 'Edited Zone used by integration tests and can be safely deleted' + }, true); + }); + + casper.waitForSelector('#zone-description', function () { + test.assertUrlMatch(/zoninator&action=edit&zone_id=/, 'Then Edit Zone Page is Rendered'); + test.assertSelectorHasText('#zone-description', + 'Edited Zone used by integration tests and can be safely deleted', + 'And the Edited Zone contains expected description'); + + }); + + var recentPosts = []; + + casper.waitForSelector('#zone-post-latest', function () { + recentPosts = this.getElementsAttribute('#zone-post-latest > option', 'value'); + this.echo('And the following recent posts are available: [' + recentPosts + ']'); + test.assertTrue(recentPosts.length > 2, + 'And I have at least two posts that can be added to the Zone'); + }); + + casper.then(function () { + this.assertCanAddPostToZone(recentPosts[1]); + }); + + casper.then(function () { + this.assertCanAddPostToZone(recentPosts[2]); + }); + + casper.waitForSelector('#zone-post-latest', function () { + var recentPostsAfterAdditions = this.getElementsAttribute('#zone-post-latest > option', 'value'); + var selectBoxDifference = recentPosts.length - recentPostsAfterAdditions.length; + test.assertTrue(selectBoxDifference === 2, 'Then the selectable recent posts decrease by 2'); + }); + + casper.then(function () { + this.assertPostCanBeRemovedFromZone(recentPosts[2]); + }); + + casper.then(function () { + this.assertPostCanBeRemovedFromZone(recentPosts[1]); + }); + + casper.waitForSelector('#zone-post-latest', function () { + var recentPostsAfterAdditions = this.getElementsAttribute('#zone-post-latest > option', 'value'); + var selectBoxDifference = recentPosts.length - recentPostsAfterAdditions.length; + test.assertTrue(selectBoxDifference === 2, 'Then the selectable recent posts do not increase by 2 (and this is unexpected)'); + }); + + // Deleting a zone + + casper.then(function () { + this.clickLabel(integrationTestZoneTitle, 'span'); + }); + + casper.then(function () { + this.clickLabel('Delete','a'); + }); + + casper.then(function () { + test.assertTrue(this.zoneDeleted(), 'A Zone can be deleted'); + }); + + casper.run(function() { + test.done(); + }); + } +}); diff --git a/tests/bin/install-wp-tests.sh b/tests/bin/install-wp-tests.sh new file mode 100755 index 0000000..74bddb8 --- /dev/null +++ b/tests/bin/install-wp-tests.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash + +if [ $# -lt 3 ]; then + echo "usage: $0 [db-host] [wp-version]" + exit 1 +fi + +DB_NAME=$1 +DB_USER=$2 +DB_PASS=$3 +DB_HOST=${4-localhost} +WP_VERSION=${5-latest} + +WP_TESTS_DIR=${WP_TESTS_DIR-/tmp/wordpress-tests-lib} +WP_CORE_DIR=${WP_CORE_DIR-/tmp/wordpress/} + +download() { + if [ `which curl` ]; then + curl -s "$1" > "$2"; + elif [ `which wget` ]; then + wget -nv -O "$2" "$1" + fi +} + +if [[ $WP_VERSION =~ [0-9]+\.[0-9]+(\.[0-9]+)? ]]; then + WP_TESTS_TAG="tags/$WP_VERSION" +elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then + WP_TESTS_TAG="trunk" +else + # http serves a single offer, whereas https serves multiple. we only want one + download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json + grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json + LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') + if [[ -z "$LATEST_VERSION" ]]; then + echo "Latest WordPress version could not be found" + exit 1 + fi + WP_TESTS_TAG="tags/$LATEST_VERSION" +fi + +set -ex + +install_wp() { + + if [ -d $WP_CORE_DIR ]; then + return; + fi + + mkdir -p $WP_CORE_DIR + + if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then + mkdir -p /tmp/wordpress-nightly + download https://wordpress.org/nightly-builds/wordpress-latest.zip /tmp/wordpress-nightly/wordpress-nightly.zip + unzip -q /tmp/wordpress-nightly/wordpress-nightly.zip -d /tmp/wordpress-nightly/ + mv /tmp/wordpress-nightly/wordpress/* $WP_CORE_DIR + else + if [ $WP_VERSION == 'latest' ]; then + local ARCHIVE_NAME='latest' + else + local ARCHIVE_NAME="wordpress-$WP_VERSION" + fi + download https://wordpress.org/${ARCHIVE_NAME}.tar.gz /tmp/wordpress.tar.gz + tar --strip-components=1 -zxmf /tmp/wordpress.tar.gz -C $WP_CORE_DIR + fi + + download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php +} + +install_test_suite() { + # portable in-place argument for both GNU sed and Mac OSX sed + if [[ $(uname -s) == 'Darwin' ]]; then + local ioption='-i .bak' + else + local ioption='-i' + fi + + # set up testing suite if it doesn't yet exist + if [ ! -d $WP_TESTS_DIR ]; then + # set up testing suite + mkdir -p $WP_TESTS_DIR + svn co https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes + fi + + cd $WP_TESTS_DIR + + if [ ! -f wp-tests-config.php ]; then + download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR':" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php + fi + +} + +install_db() { + # parse DB_HOST for port or socket references + local PARTS=(${DB_HOST//\:/ }) + local DB_HOSTNAME=${PARTS[0]}; + local DB_SOCK_OR_PORT=${PARTS[1]}; + local EXTRA="" + + if ! [ -z $DB_HOSTNAME ] ; then + if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then + EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" + elif ! [ -z $DB_SOCK_OR_PORT ] ; then + EXTRA=" --socket=$DB_SOCK_OR_PORT" + elif ! [ -z $DB_HOSTNAME ] ; then + EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" + fi + fi + + # create database + mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA +} + +install_wp +install_test_suite +install_db diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100755 index 0000000..816dcfa --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,19 @@ + dev/null + +echo "Setting up the test env for zoninator. Checking dependencies"; + +which svn > /dev/null + +if [ $? -ne 0 ]; then + echo "cannot find subversion. please install subversion"; + exit 1; +fi + +which wget > /dev/null + +if [ $? -ne 0 ]; then + echo "cannot find wget. please install wget"; + exit 1; +fi + +which composer > /dev/null + +if [ $? -ne 0 ]; then + echo "cannot find composer. please install composer"; + exit 1; +fi + +echo "installing composer dependencies"; + +composer install > /dev/null + +bash bin/install-wp-tests.sh zoninator_test root password localhost latest diff --git a/tests/unit/class-zoninator-api-controller-test.php b/tests/unit/class-zoninator-api-controller-test.php new file mode 100644 index 0000000..7b3f074 --- /dev/null +++ b/tests/unit/class-zoninator-api-controller-test.php @@ -0,0 +1,532 @@ +assertEquals( $status_code, $response->get_status() ); + } + + /** + * As Admin + * + * @return Zoninator_Api_Controller_Test + */ + function login_as_admin() { + return $this->login_as( $this->admin_id ); + } + + + /** + * Login as + * + * @param int $user_id U. + * @return Zoninator_Api_Controller_Test $this + */ + function login_as( $user_id ) { + wp_set_current_user( $user_id ); + return $this; + } + + /** + * Assert Status 200 + * + * @param WP_REST_Response $response Response. + */ + function assert_http_response_status_success( $response ) { + $this->assert_response_status( $response, MT_Controller::HTTP_OK ); + } + + /** + * Assert Status 201 + * + * @param WP_REST_Response $response Response. + */ + function assert_http_response_status_created( $response ) { + $this->assert_response_status( $response, MT_Controller::HTTP_CREATED ); + } + + /** + * Assert Status 404 + * + * @param WP_REST_Response $response Response. + */ + function assert_http_response_status_not_found( $response ) { + $this->assert_response_status( $response, MT_Controller::HTTP_NOT_FOUND ); + } + + /** + * Ensure we got a certain response code + * + * @param WP_REST_Response $response The Response. + * @param int $status_code Expected status code. + */ + function assertResponseStatus( $response, $status_code ) { + $this->assertInstanceOf( 'WP_REST_Response', $response ); + $this->assertEquals( $status_code, $response->get_status() ); + } + + /** + * Have WP_REST_Server Dispatch an HTTP request + * + * @param string $endpoint The Endpoint. + * @param string $method Http mehod. + * @param array $args Any Data/Args. + * @return WP_REST_Response + */ + function request( $endpoint, $method, $args = array() ) { + $request = new WP_REST_Request( $method, $endpoint ); + foreach ( $args as $key => $value ) { + $request->set_param( $key, $value ); + } + return $this->rest_server->dispatch( $request ); + } + + /** + * Have WP_REST_Server Dispatch a GET HTTP request + * + * @param string $endpoint The Endpoint. + * @param array $args Any Data/Args. + * @return WP_REST_Response + */ + function get( $endpoint, $args = array() ) { + return $this->request( $endpoint, 'GET', $args ); + } + + /** + * Have WP_REST_Server Dispatch a POST HTTP request + * + * @param string $endpoint The Endpoint. + * @param array $args Any Data/Args. + * @return WP_REST_Response + */ + function post( $endpoint, $args = array() ) { + return $this->request( $endpoint, 'POST', $args ); + } + + /** + * Have WP_REST_Server Dispatch a PUT HTTP request + * + * @param string $endpoint The Endpoint. + * @param array $args Any Data/Args. + * @return WP_REST_Response + */ + function put( $endpoint, $args = array() ) { + return $this->request( $endpoint, 'PUT', $args ); + } + + /** + * Have WP_REST_Server Dispatch a DELETE HTTP request + * + * @param string $endpoint The Endpoint. + * @param array $args Any Data/Args. + * @return WP_REST_Response + */ + function delete( $endpoint, $args = array() ) { + return $this->request( $endpoint, 'DELETE', $args ); + } + + /** + * Setup + */ + function setUp() { + parent::setUp(); + /** + *The global + * + * @var WP_REST_Server $wp_rest_server + */ + global $wp_rest_server; + $this->rest_server = new Spy_REST_Server; + $wp_rest_server = $this->rest_server; + $this->_zoninator = Zoninator(); + $admin = get_user_by( 'email', 'rest_api_admin_user@test.com' ); + if ( false === $admin ) { + $this->admin_id = wp_create_user( + 'rest_api_admin_user', + 'rest_api_admin_user', + 'rest_api_admin_user@test.com' + ); + $admin = get_user_by( 'ID', $this->admin_id ); + $admin->set_role( 'administrator' ); + } + + $this->default_user_id = get_current_user_id(); + $this->login_as_admin(); + $this->rest_server = $wp_rest_server; + do_action( 'rest_api_init' ); + $this->environment = Zoninator()->rest_api->bootstrap->environment(); + } + + /** + * T test_add_post_to_zone_responds_with_created_when_method_post + * + * @throws Exception E. + */ + function test_add_post_to_zone_responds_with_created_when_method_post() { + $this->login_as_admin(); + $post_id = $this->_insert_a_post(); + $zone_id = $this->create_a_zone( 'the-zone-add-post-1', 'The Zone Add Post one' ); + $response = $this->post( '/zoninator/v1/zones/' . $zone_id . '/posts', array( + 'post_id' => $post_id, + ) ); + $this->assertResponseStatus( $response, 201 ); + } + + /** + * T test_add_post_to_zone_responds_with_created_when_method_put + * + * @throws Exception E. + */ + function test_add_post_to_zone_responds_with_success_when_method_put() { + $this->login_as_admin(); + $post_id = $this->_insert_a_post(); + $zone_id = $this->create_a_zone( 'the-zone-add-post-1', 'The Zone Add Post one' ); + $response = $this->put( '/zoninator/v1/zones/' . $zone_id . '/posts', array( + 'post_id' => $post_id, + ) ); + $this->assertResponseStatus( $response, 200 ); + } + + /** + * T test_add_post_to_zone_respond_not_found_if_zone_not_exists + * + * @throws Exception E. + */ + function test_add_post_to_zone_respond_not_found_if_zone_not_exists() { + $post_id = $this->_insert_a_post(); + $response = $this->put( '/zoninator/v1/zones/666666/posts', array( + 'post_id' => $post_id, + ) ); + $this->assertResponseStatus( $response, 404 ); + } + + /** + * Test test_add_post_to_zone_fail_if_invalid_post + */ + function test_add_post_to_zone_fail_if_invalid_post() { + $zone_id = $this->add_a_zone( 'zone-test_add_post_to_zone_fail_if_invalid_post' ); + $response = $this->post( '/zoninator/v1/zones/' . $zone_id . '/posts', array( + 'post_id' => 666666, + ) ); + $this->assertResponseStatus( $response, 400 ); + } + + /** + * T test_get_zone_posts_success_when_valid_zone_and_posts + * + * @throws Exception E. + */ + function test_get_zone_posts_success_when_valid_zone_and_posts() { + $this->login_as_admin(); + self::factory()->post->create_many( 5 ); + $query = new WP_Query(); + $posts = $query->query( array() ); + $post = $posts[0]; + $zone_id = $this->add_a_zone(); + $response = $this->post( '/zoninator/v1/zones/' . $zone_id . '/posts', array( + 'post_id' => $post->ID, + ) ); + $this->assertResponseStatus( $response, 201 ); + $response = $this->get( '/zoninator/v1/zones/' . $zone_id ); + $this->assertResponseStatus( $response, 200 ); + } + + /** + * Test test_get_zone_posts_not_found_when_invalid_zone + */ + function test_get_zone_posts_not_found_when_invalid_zone() { + $term_factory = new WP_UnitTest_Factory_For_Term( null, Zoninator()->zone_taxonomy ); + $zone_id = $term_factory->create_object( array( + 'name' => 'The Zone Add Post one', + 'description' => 'Zone 2', + 'slug' => 'zone-2', + ) ); + + $response = $this->get( '/zoninator/v1/zones/' . ( $zone_id + 3 ) ); + $this->assertResponseStatus( $response, 404 ); + } + + /** + * Test test_get_zone_posts_fail_when_no_posts_in_zone + */ + function test_get_zone_posts_fail_when_no_posts_in_zone() { + $term_factory = new WP_UnitTest_Factory_For_Term( null, Zoninator()->zone_taxonomy ); + $zone_id = $term_factory->create_object( array( + 'name' => 'The Zone Add Post one', + 'description' => 'Zone 2', + 'slug' => 'zone-2', + ) ); + + $response = $this->get( '/zoninator/v1/zones/' . $zone_id ); + $this->assertResponseStatus( $response, 400 ); + } + + /** + * Test test_remove_post_from_zone_bad_request_if_invalid_post_id + */ + function test_remove_post_from_zone_bad_request_if_invalid_post_id() { + $zone_id = $this->add_a_zone( 'zone-test_remove_post_from_zone_bad_request_if_invalid_post_id' ); + $response = $this->delete( '/zoninator/v1/zones/' . $zone_id . '/posts/0' ); + $this->assertResponseStatus( $response, 400 ); + } + + /** + * Test test_remove_post_from_zone_not_found_if_no_zone_id + */ + function test_remove_post_from_zone_not_found_if_no_zone_id() { + $response = $this->delete( '/zoninator/v1/zones/121212/posts/' ); + $this->assertResponseStatus( $response, 404 ); + } + + /** + * Test test_remove_post_from_zone_succeed_if_successful + */ + function test_remove_post_from_zone_succeed_if_successful() { + $this->login_as_admin(); + $zone_id = $this->add_a_zone( 'zone-test_remove_post_from_zone_succeed_if_successful' ); + self::factory()->post->create_many( 5 ); + $query = new WP_Query(); + $posts = $query->query( array() ); + foreach ( $posts as $post ) { + $response = $this->post( '/zoninator/v1/zones/' . $zone_id . '/posts', array( + 'post_id' => $post->ID, + ) ); + $this->assertResponseStatus( $response, 201 ); + } + + $response = $this->get( '/zoninator/v1/zones/' . $zone_id ); + $this->assertResponseStatus( $response, 200 ); + $data = $response->get_data(); + $this->assertSame( count( $posts ), count( $data ) ); + $first_post = $data[0]; + $response = $this->delete( '/zoninator/v1/zones/' . $zone_id . '/posts/' . $first_post->ID ); + $this->assertResponseStatus( $response, 200 ); + $response = $this->get( '/zoninator/v1/zones/' . $zone_id ); + $this->assertResponseStatus( $response, 200 ); + $data = $response->get_data(); + $this->assertSame( count( $posts ) - 1, count( $data ) ); + $ids = wp_list_pluck( $data, 'ID' ); + $this->assertTrue( ! in_array( $first_post->ID, $ids, true ) ); + } + + /** + * Test test_reorder_posts_on_zone_return_WP_Error_if_post_ids_not_present + */ + function test_reorder_posts_on_zone_return_WP_Error_if_post_ids_not_present() { + $this->login_as_admin(); + $zone_id = $this->add_a_zone( 'zone-test_reorder_posts_on_zone_return_WP_Error_if_post_ids_not_present' ); + self::factory()->post->create_many( 5 ); + $query = new WP_Query(); + $posts = $query->query( array() ); + foreach ( $posts as $post ) { + $response = $this->post( '/zoninator/v1/zones/' . $zone_id . '/posts', array( + 'post_id' => $post->ID, + ) ); + $this->assertResponseStatus( $response, 201 ); + } + + $response = $this->get( '/zoninator/v1/zones/' . $zone_id ); + $data = $response->get_data(); + $ids = wp_list_pluck( $data, 'ID' ); + shuffle( $ids ); + $request_data = array(); + $response = $this->put( '/zoninator/v1/zones/' . $zone_id . '/posts/order', $request_data ); + $this->assertResponseStatus( $response, 400 ); + } + + /** + * Test test_reorder_posts_on_zone_success + */ + function test_reorder_posts_on_zone_success() { + $this->login_as_admin(); + $zone_id = $this->add_a_zone( 'zone-test_reorder_posts_on_zone_success' ); + self::factory()->post->create_many( 5 ); + $query = new WP_Query(); + $posts = $query->query( array() ); + foreach ( $posts as $post ) { + $response = $this->post( '/zoninator/v1/zones/' . $zone_id . '/posts', array( + 'post_id' => $post->ID, + ) ); + $this->assertResponseStatus( $response, 201 ); + } + + $response = $this->get( '/zoninator/v1/zones/' . $zone_id ); + $data = $response->get_data(); + $ids = wp_list_pluck( $data, 'ID' ); + shuffle( $ids ); + $request_data = array( + 'posts' => $ids, + ); + + $response = $this->put( '/zoninator/v1/zones/' . $zone_id . '/posts/order', $request_data ); + $this->assertResponseStatus( $response, 200 ); + + $response = $this->get( '/zoninator/v1/zones/' . $zone_id ); + $data = $response->get_data(); + $reordered_ids = wp_list_pluck( $data, 'ID' ); + + $this->assertEquals( $ids, $reordered_ids ); + } + + /** + * Test test_reorder_posts_on_zone_return_WP_Error_if_zone_id_not_present + */ + function test_reorder_posts_on_zone_return_WP_Error_if_zone_id_not_present() { + $response = $this->put( '/zoninator/v1/zones/123123/posts/order', array() ); + $this->assertResponseStatus( $response, 404 ); + } +// +// function test_zone_update_lock_200() +// { +// $this->_zoninator->method( 'is_zone_locked' )->willReturn( false ); +// $request = $this->_create_request( array( 'zone_id' => 3 ) ); +// $response = $this->_controller->zone_update_lock( $request ); +// $this->_assert_response_status($response, 200); +// } +// +// function test_zone_update_lock_400_if_zone_already_locked() +// { +// $this->_zoninator->method( 'is_zone_locked' )->willReturn( true ); +// $request = $this->_create_request( array( 'zone_id' => 3 ) ); +// $response = $this->_controller->zone_update_lock( $request ); +// $this->_assert_response_status($response, 400); +// } +// +// function test_zone_update_error_if_no_zone_id() +// { +// $this->_zoninator->method( 'is_zone_locked' )->willReturn( false ); +// $request = $this->_create_request( array( ) ); +// $response = $this->_controller->zone_update_lock( $request ); +// $this->assertInstanceOf( 'WP_Error', $response ); +// } +// + /** + * Test test_search_posts_error_if_empty_term + */ + function test_search_posts_return_results() { + self::factory()->post->create_many( 5 ); + $query = new WP_Query(); + $posts = $query->query( array() ); + $first = $posts[0]; + $data = array( 'term' => $first->post_title ); + $response = $this->get( '/zoninator/v1/posts/search', $data ); + $this->assertResponseStatus( $response, 200 ); + $data = $response->get_data(); + $first_result = $data[0]; + $this->assertEquals( $first->ID, $first_result['post_id'] ); + } + + /** + * Test test_search_posts_error_if_empty_term + */ + function test_search_posts_error_if_empty_term() { + self::factory()->post->create_many( 5 ); + $query = new WP_Query(); + $posts = $query->query( array() ); + $data = array( 'term' => '' ); + $response = $this->get( '/zoninator/v1/posts/search', $data ); + $this->assertResponseStatus( $response, 400 ); + } + + /** + * @return int|WP_Error + */ + private function _insert_a_post() { + $insert = wp_insert_post( array( + 'post_content' => 'Content For this post ' . rand_str(), + 'post_title' => 'Title ' . rand_str(), + 'post_excerpt' => 'Excerpt ' . rand_str(), + 'post_status' => 'published', + 'post_type' => 'post' + ) ); + if ( is_wp_error( $insert ) ) { + throw new Exception( 'Error' ); + } + return $insert; + } + + /** + * @param array $params + * @return WP_REST_Request + */ + private function _create_request(array $params = array()) + { + $request = new WP_REST_Request( + WP_REST_Server::CREATABLE, + '' + ); + + foreach ($params as $key => $value) { + $request->set_param( $key, $value ); + } + return $request; + } + + /** + * @param $response + * @param int $status + */ + private function _assert_response_status($response, $status = 200) { + $this->assertInstanceOf('WP_REST_Response', $response); + $this->assertEquals($status, $response->get_status()); + } + + private function create_a_zone( $slug, $title ) { + $result = Zoninator()->insert_zone( $slug, $title, array( 'description' => rand_str() ) ); + if ( is_wp_error( $result ) ) { + return $result; + } + return isset( $result['term_id'] ) ? $result['term_id'] : 0; + } + + /** + * Add A Zone + * + * @param string $slug Slug. + * + * @return array|mixed|WP_Error + */ + private function add_a_zone( $slug = 'zone-1' ) { + $term_factory = new WP_UnitTest_Factory_For_Term(null, Zoninator()->zone_taxonomy); + $zone_id = $term_factory->create_object(array( + 'name' => 'The Zone Add Post one ' . rand_str(), + 'description' => 'Zone ' . rand_str(), + 'slug' => $slug, + )); + return $zone_id; + } +} From 0e2b91077a1814909b68eee9e81c180b0c77428a Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Wed, 2 Aug 2017 17:55:36 +0300 Subject: [PATCH 05/31] get latest build_mixtape script --- scripts/build_mixtape.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/build_mixtape.sh b/scripts/build_mixtape.sh index c1d6047..e94423e 100755 --- a/scripts/build_mixtape.sh +++ b/scripts/build_mixtape.sh @@ -80,6 +80,10 @@ echo "PREFIX = $mt_current_prefix"; echo "DESTINATION = $mt_current_destination"; echo ""; +if [ ! -d "$mt_current_destination" ]; then + mkdir -p "$mt_current_destination" +fi + expect_directory "$mt_current_destination"; cd $MIXTAPE_PATH; From 65b43b1748444993cf56e641493cb9a403093d9c Mon Sep 17 00:00:00 2001 From: Kuba Birecki Date: Sun, 30 Jul 2017 15:12:41 +0200 Subject: [PATCH 06/31] Add 'get_zones' endpoint --- includes/class-zoninator-api-controller.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/includes/class-zoninator-api-controller.php b/includes/class-zoninator-api-controller.php index e2db02f..fda576a 100644 --- a/includes/class-zoninator-api-controller.php +++ b/includes/class-zoninator-api-controller.php @@ -60,6 +60,9 @@ function setup() { self::ZONE_ID_POST_ID_REQUIRED => __( 'post id and zone id required', 'zoninator' ), ); + $this->add_route( 'zones' ) + ->add_action( $this->action( 'index', 'get_zones' ) ); + $this->add_route( 'zones/(?P[\d]+)' ) ->add_action( $this->action( 'index', 'get_zone_posts' ) ->permissions( 'get_zone_posts_permissions_check' ) @@ -100,6 +103,22 @@ function setup() { ->args( '_params_for_get_recent_posts' ) ); } + /** + * Get the list of all zones + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|WP_REST_Response + */ + function get_zones( $request ) { + $results = $this->instance->get_zones(); + + if ( is_wp_error( $results ) ) { + return $this->_bad_request( self::ZONE_FEED_ERROR, $results->get_error_message() ); + } + + return new WP_REST_Response( $results, 200 ); + } + /** * Add a post to zone * From e30eaaf40ee08ce002c040ccb65ae48f795e2185 Mon Sep 17 00:00:00 2001 From: Kuba Birecki Date: Wed, 2 Aug 2017 17:28:53 +0200 Subject: [PATCH 07/31] Fix typo --- includes/class-zoninator-api-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-zoninator-api-controller.php b/includes/class-zoninator-api-controller.php index fda576a..7890abc 100644 --- a/includes/class-zoninator-api-controller.php +++ b/includes/class-zoninator-api-controller.php @@ -1,6 +1,6 @@ Date: Thu, 3 Aug 2017 12:02:31 +0200 Subject: [PATCH 08/31] Add create_zone endpoint --- includes/class-zoninator-api-controller.php | 88 +++++++++++++++++++-- 1 file changed, 82 insertions(+), 6 deletions(-) diff --git a/includes/class-zoninator-api-controller.php b/includes/class-zoninator-api-controller.php index 7890abc..d28feef 100644 --- a/includes/class-zoninator-api-controller.php +++ b/includes/class-zoninator-api-controller.php @@ -3,7 +3,6 @@ * @package Zoninator/Rest */ - /** * Class Zoninator_Api_Controller */ @@ -22,6 +21,7 @@ class Zoninator_Api_Controller extends Zoninator_REST_Controller { const PERMISSION_DENIED = 'permission-denied'; const ZONE_NOT_FOUND = 'zone-not-found'; const POST_NOT_FOUND = 'post-not-found'; + const INVALID_ZONE_SETTINGS = 'invalid-zone-settings'; /** * Instance * @@ -50,9 +50,6 @@ function __construct( $instance ) { * Set up this controller */ function setup() { -// $this->add_route( 'zones' ) -// ->add_action( $this->action( 'index', 'get_zones' ) ) -// ->add_action( $this->action( 'create', 'create_zone' ) ); $this->translations = array( self::ZONE_NOT_FOUND => __( 'Zone not found', 'zoninator' ), self::INVALID_POST_ID => __( 'Invalid post id', 'zoninator' ), @@ -61,7 +58,15 @@ function setup() { ); $this->add_route( 'zones' ) - ->add_action( $this->action( 'index', 'get_zones' ) ); + ->add_action( + $this->action( 'index', 'get_zones' ) + ->permissions( 'get_zones_permissions_check' ) + ) + ->add_action( + $this->action( 'create', 'create_zone' ) + ->permissions( 'add_zone_permissions_check' ) + ->args( '_params_for_create_zone' ) + ); $this->add_route( 'zones/(?P[\d]+)' ) ->add_action( $this->action( 'index', 'get_zone_posts' ) @@ -113,12 +118,36 @@ function get_zones( $request ) { $results = $this->instance->get_zones(); if ( is_wp_error( $results ) ) { - return $this->_bad_request( self::ZONE_FEED_ERROR, $results->get_error_message() ); + return $this->_bad_request( array( + 'message' => $results->get_error_message() + ) ); } return new WP_REST_Response( $results, 200 ); } + /** + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|WP_REST_Response + */ + function create_zone( $request ) { + $name = $this->_get_param( $request, 'name', '' ); + $slug = $this->_get_param( $request, 'slug', '' ); + $description = $this->_get_param( $request, 'description', '', 'strip_tags' ); + + $result = $this->instance->insert_zone( $slug, $name, array( 'description' => $description ) ); + + if ( is_wp_error( $result ) ) { + return $this->bad_request( array( + 'message' => $result->get_error_message() + ) ); + } + + $zone = $this->instance->get_zone( $result[ 'term_id' ] ); + + return $this->created( $zone ); + } + /** * Add a post to zone * @@ -445,6 +474,26 @@ public function get_recent_posts( $request ) { return $response; } + /** + * Check if a given request has access to the zones index. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|bool + */ + public function get_zones_permissions_check( $request ) { + return true; + } + + /** + * Check if a given request has access to add new zones. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|bool + */ + public function add_zone_permissions_check( $request ) { + return $this->_permissions_check( 'insert' ); + } + /** * Check if a given request has access to get zone posts. * @@ -498,6 +547,11 @@ public function strip_slashes( $item ) { return stripslashes( $item ); } + public function strip_tags( $item ) { + // see https://github.com/WP-API/WP-API/issues/1520 on why we do not use stripslashes directly + return strip_tags( $item ); + } + /** * @param WP_REST_Request $object * @param $var @@ -517,6 +571,28 @@ private function _get_param( $object, $var, $default = '', $sanitize_callback = return $value; } + public function _params_for_create_zone() { + return array( + 'name' => array( + 'type' => 'string', + 'sanitize_callback' => array( $this, 'strip_slashes' ), + 'default' => '', + 'required' => false + ), + 'slug' => array( + 'type' => 'string', + 'sanitize_callback' => array( $this, 'strip_slashes' ), + 'required' => true, + ), + 'description' => array( + 'type' => 'string', + 'sanitize_callback' => array( $this, 'strip_tags' ), + 'default' => '', + 'required' => false, + ) + ); + } + public function _get_zone_id_param() { return array( 'zone_id' => array( From 6cbfe4b7bf6e47a501b18bc61d7564cdb796485a Mon Sep 17 00:00:00 2001 From: Kuba Birecki Date: Thu, 3 Aug 2017 12:03:04 +0200 Subject: [PATCH 09/31] Use 'update' permissions for 'add_post_to_zone' --- includes/class-zoninator-api-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-zoninator-api-controller.php b/includes/class-zoninator-api-controller.php index d28feef..a5a5dae 100644 --- a/includes/class-zoninator-api-controller.php +++ b/includes/class-zoninator-api-controller.php @@ -523,7 +523,7 @@ public function remove_post_from_zone_permissions_check( $request ) { */ public function add_post_to_zone_permissions_check( $request ) { $zone_id = $this->_get_param( $request, 'zone_id', 0, 'absint' ); - return $this->_permissions_check( 'insert', $zone_id ); + return $this->_permissions_check( 'update', $zone_id ); } /** From 3371bd9e1a73c42de14a22e078a2464b6d9f0168 Mon Sep 17 00:00:00 2001 From: Kuba Birecki Date: Thu, 3 Aug 2017 13:00:19 +0200 Subject: [PATCH 10/31] Add tests --- .../class-zoninator-api-controller-test.php | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/unit/class-zoninator-api-controller-test.php b/tests/unit/class-zoninator-api-controller-test.php index 7b3f074..f6b1476 100644 --- a/tests/unit/class-zoninator-api-controller-test.php +++ b/tests/unit/class-zoninator-api-controller-test.php @@ -49,7 +49,6 @@ function login_as_admin() { return $this->login_as( $this->admin_id ); } - /** * Login as * @@ -191,6 +190,33 @@ function setUp() { $this->environment = Zoninator()->rest_api->bootstrap->environment(); } + /** + * T test_create_zone_responds_with_created_when_method_post + * + * @throws Exception E. + */ + function test_create_zone_responds_with_created_when_method_post() { + $this->login_as_admin(); + $response = $this->post( '/zoninator/v1/zones', array( + 'slug' => 'test-zone', + ) ); + $this->assertResponseStatus( $response, 201 ); + } + + /** + * T test_create_zone_fail_if_invalid_data + * + * @throws Exception E. + */ + function test_create_zone_fail_if_invalid_data() { + $this->login_as_admin(); + $response = $this->post( '/zoninator/v1/zones', array( + 'name' => 'Test zone', + 'description' => 'No slug provided.' + ) ); + $this->assertResponseStatus( $response, 400 ); + } + /** * T test_add_post_to_zone_responds_with_created_when_method_post * From ea1dd848830a8a70cbe0f0f204fe53d2a907741a Mon Sep 17 00:00:00 2001 From: Kuba Birecki Date: Thu, 3 Aug 2017 14:17:50 +0200 Subject: [PATCH 11/31] Update code style --- includes/class-zoninator-api-controller.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/includes/class-zoninator-api-controller.php b/includes/class-zoninator-api-controller.php index a5a5dae..a27b79a 100644 --- a/includes/class-zoninator-api-controller.php +++ b/includes/class-zoninator-api-controller.php @@ -123,7 +123,7 @@ function get_zones( $request ) { ) ); } - return new WP_REST_Response( $results, 200 ); + return $this->ok( $results ); } /** @@ -135,7 +135,9 @@ function create_zone( $request ) { $slug = $this->_get_param( $request, 'slug', '' ); $description = $this->_get_param( $request, 'description', '', 'strip_tags' ); - $result = $this->instance->insert_zone( $slug, $name, array( 'description' => $description ) ); + $result = $this->instance->insert_zone( $slug, $name, array( + 'description' => $description, + ) ); if ( is_wp_error( $result ) ) { return $this->bad_request( array( @@ -548,7 +550,7 @@ public function strip_slashes( $item ) { } public function strip_tags( $item ) { - // see https://github.com/WP-API/WP-API/issues/1520 on why we do not use stripslashes directly + // see https://github.com/WP-API/WP-API/issues/1520 on why we do not use strip_tags directly return strip_tags( $item ); } From ea20d5e976d9007ecce4fe90becb8979105c055f Mon Sep 17 00:00:00 2001 From: Kuba Birecki Date: Fri, 4 Aug 2017 12:07:05 +0200 Subject: [PATCH 12/31] Remove unnecessary properties from zones index response. --- includes/class-zoninator-api-controller.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/includes/class-zoninator-api-controller.php b/includes/class-zoninator-api-controller.php index a27b79a..fbcb4e8 100644 --- a/includes/class-zoninator-api-controller.php +++ b/includes/class-zoninator-api-controller.php @@ -123,7 +123,9 @@ function get_zones( $request ) { ) ); } - return $this->ok( $results ); + $zones = array_map( array( $this, '_filter_zone_properties' ), $results ); + + return $this->ok( $zones ); } /** @@ -680,6 +682,17 @@ public function _params_for_search_posts() { // ); } + public function _filter_zone_properties( $zone ) { + $data = $zone->to_array(); + + return array( + 'term_id' => $data[ 'term_id' ], + 'slug' => $data[ 'slug' ], + 'name' => $data[ 'name' ], + 'description' => $data[ 'description' ], + ); + } + private function _bad_request($code, $message) { return new WP_Error( $code, $message, array( 'status' => 400 ) ); } From 90b25a7900143997fc870cb53650e0e66caaac38 Mon Sep 17 00:00:00 2001 From: Kuba Birecki Date: Wed, 9 Aug 2017 12:11:46 +0200 Subject: [PATCH 13/31] Use name as the default for slug on create_zone --- includes/class-zoninator-api-controller.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/includes/class-zoninator-api-controller.php b/includes/class-zoninator-api-controller.php index fbcb4e8..240999e 100644 --- a/includes/class-zoninator-api-controller.php +++ b/includes/class-zoninator-api-controller.php @@ -134,7 +134,7 @@ function get_zones( $request ) { */ function create_zone( $request ) { $name = $this->_get_param( $request, 'name', '' ); - $slug = $this->_get_param( $request, 'slug', '' ); + $slug = $this->_get_param( $request, 'slug', $name ); $description = $this->_get_param( $request, 'description', '', 'strip_tags' ); $result = $this->instance->insert_zone( $slug, $name, array( @@ -149,7 +149,7 @@ function create_zone( $request ) { $zone = $this->instance->get_zone( $result[ 'term_id' ] ); - return $this->created( $zone ); + return $this->created( $this->_filter_zone_properties( $zone ) ); } /** @@ -586,7 +586,8 @@ public function _params_for_create_zone() { 'slug' => array( 'type' => 'string', 'sanitize_callback' => array( $this, 'strip_slashes' ), - 'required' => true, + 'default' => '', + 'required' => false, ), 'description' => array( 'type' => 'string', From 5a6163f5b16d6dbebacaf39c665b4c46155a6dce Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Fri, 11 Aug 2017 14:13:57 +0300 Subject: [PATCH 14/31] Suffix get_zone_posts with /posts --- includes/class-zoninator-api-controller.php | 14 +++--- .../class-zoninator-api-controller-test.php | 43 +++++-------------- 2 files changed, 17 insertions(+), 40 deletions(-) diff --git a/includes/class-zoninator-api-controller.php b/includes/class-zoninator-api-controller.php index 240999e..0b91cf6 100644 --- a/includes/class-zoninator-api-controller.php +++ b/includes/class-zoninator-api-controller.php @@ -68,13 +68,11 @@ function setup() { ->args( '_params_for_create_zone' ) ); - $this->add_route( 'zones/(?P[\d]+)' ) + $this->add_route( 'zones/(?P[\d]+)/posts' ) ->add_action( $this->action( 'index', 'get_zone_posts' ) ->permissions( 'get_zone_posts_permissions_check' ) ->args( '_get_zone_id_param' ) - ); - - $this->add_route( 'zones/(?P[\d]+)/posts' ) + ) ->add_action( $this->action( 'create', 'add_post_to_zone' ) ->permissions( 'add_post_to_zone_permissions_check' ) ->args( '_get_zone_post_rest_route_params' ) @@ -118,8 +116,8 @@ function get_zones( $request ) { $results = $this->instance->get_zones(); if ( is_wp_error( $results ) ) { - return $this->_bad_request( array( - 'message' => $results->get_error_message() + return $this->bad_request( array( + 'message' => $results->get_error_message(), ) ); } @@ -129,6 +127,8 @@ function get_zones( $request ) { } /** + * Create a Zone + * * @param WP_REST_Request $request Full data about the request. * @return WP_Error|WP_REST_Response */ @@ -143,7 +143,7 @@ function create_zone( $request ) { if ( is_wp_error( $result ) ) { return $this->bad_request( array( - 'message' => $result->get_error_message() + 'message' => $result->get_error_message(), ) ); } diff --git a/tests/unit/class-zoninator-api-controller-test.php b/tests/unit/class-zoninator-api-controller-test.php index f6b1476..a7c0bca 100644 --- a/tests/unit/class-zoninator-api-controller-test.php +++ b/tests/unit/class-zoninator-api-controller-test.php @@ -287,7 +287,7 @@ function test_get_zone_posts_success_when_valid_zone_and_posts() { 'post_id' => $post->ID, ) ); $this->assertResponseStatus( $response, 201 ); - $response = $this->get( '/zoninator/v1/zones/' . $zone_id ); + $response = $this->get( '/zoninator/v1/zones/' . $zone_id . '/posts' ); $this->assertResponseStatus( $response, 200 ); } @@ -317,7 +317,7 @@ function test_get_zone_posts_fail_when_no_posts_in_zone() { 'slug' => 'zone-2', ) ); - $response = $this->get( '/zoninator/v1/zones/' . $zone_id ); + $response = $this->get( '/zoninator/v1/zones/' . $zone_id . '/posts' ); $this->assertResponseStatus( $response, 400 ); } @@ -354,14 +354,14 @@ function test_remove_post_from_zone_succeed_if_successful() { $this->assertResponseStatus( $response, 201 ); } - $response = $this->get( '/zoninator/v1/zones/' . $zone_id ); + $response = $this->get( '/zoninator/v1/zones/' . $zone_id . '/posts' ); $this->assertResponseStatus( $response, 200 ); $data = $response->get_data(); $this->assertSame( count( $posts ), count( $data ) ); $first_post = $data[0]; $response = $this->delete( '/zoninator/v1/zones/' . $zone_id . '/posts/' . $first_post->ID ); $this->assertResponseStatus( $response, 200 ); - $response = $this->get( '/zoninator/v1/zones/' . $zone_id ); + $response = $this->get( '/zoninator/v1/zones/' . $zone_id . '/posts' ); $this->assertResponseStatus( $response, 200 ); $data = $response->get_data(); $this->assertSame( count( $posts ) - 1, count( $data ) ); @@ -385,7 +385,8 @@ function test_reorder_posts_on_zone_return_WP_Error_if_post_ids_not_present() { $this->assertResponseStatus( $response, 201 ); } - $response = $this->get( '/zoninator/v1/zones/' . $zone_id ); + $response = $this->get( '/zoninator/v1/zones/' . $zone_id . '/posts' ); + $this->assertResponseStatus( $response, 200 ); $data = $response->get_data(); $ids = wp_list_pluck( $data, 'ID' ); shuffle( $ids ); @@ -410,7 +411,8 @@ function test_reorder_posts_on_zone_success() { $this->assertResponseStatus( $response, 201 ); } - $response = $this->get( '/zoninator/v1/zones/' . $zone_id ); + $response = $this->get( '/zoninator/v1/zones/' . $zone_id . '/posts' ); + $this->assertResponseStatus( $response, 200 ); $data = $response->get_data(); $ids = wp_list_pluck( $data, 'ID' ); shuffle( $ids ); @@ -421,7 +423,8 @@ function test_reorder_posts_on_zone_success() { $response = $this->put( '/zoninator/v1/zones/' . $zone_id . '/posts/order', $request_data ); $this->assertResponseStatus( $response, 200 ); - $response = $this->get( '/zoninator/v1/zones/' . $zone_id ); + $response = $this->get( '/zoninator/v1/zones/' . $zone_id . '/posts' ); + $this->assertResponseStatus( $response, 200 ); $data = $response->get_data(); $reordered_ids = wp_list_pluck( $data, 'ID' ); @@ -505,32 +508,6 @@ private function _insert_a_post() { return $insert; } - /** - * @param array $params - * @return WP_REST_Request - */ - private function _create_request(array $params = array()) - { - $request = new WP_REST_Request( - WP_REST_Server::CREATABLE, - '' - ); - - foreach ($params as $key => $value) { - $request->set_param( $key, $value ); - } - return $request; - } - - /** - * @param $response - * @param int $status - */ - private function _assert_response_status($response, $status = 200) { - $this->assertInstanceOf('WP_REST_Response', $response); - $this->assertEquals($status, $response->get_status()); - } - private function create_a_zone( $slug, $title ) { $result = Zoninator()->insert_zone( $slug, $title, array( 'description' => rand_str() ) ); if ( is_wp_error( $result ) ) { From 764a2550258b2f9346ec4ab6f0bc25f074942b7d Mon Sep 17 00:00:00 2001 From: Kuba Birecki Date: Tue, 29 Aug 2017 12:28:23 +0200 Subject: [PATCH 15/31] Fix _get_params not always assigning a default value. --- includes/class-zoninator-api-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-zoninator-api-controller.php b/includes/class-zoninator-api-controller.php index 0b91cf6..21c04a2 100644 --- a/includes/class-zoninator-api-controller.php +++ b/includes/class-zoninator-api-controller.php @@ -565,7 +565,7 @@ public function strip_tags( $item ) { */ private function _get_param( $object, $var, $default = '', $sanitize_callback = '' ) { $value = $object->get_param( $var ); - $value = ( $value !== null ) ? $value : $default; + $value = empty( $value ) ? $default : $value; if ( is_callable( $sanitize_callback ) ) { From ebaef7bf4c2ea3dd9241fb543598116003a893e5 Mon Sep 17 00:00:00 2001 From: Kuba Birecki Date: Wed, 30 Aug 2017 17:06:24 +0200 Subject: [PATCH 16/31] Combine adding, removin and reordering zone posts into update_zone --- includes/class-zoninator-api-controller.php | 191 +++--------------- .../class-zoninator-api-controller-test.php | 168 +++------------ 2 files changed, 59 insertions(+), 300 deletions(-) diff --git a/includes/class-zoninator-api-controller.php b/includes/class-zoninator-api-controller.php index 21c04a2..fcd48be 100644 --- a/includes/class-zoninator-api-controller.php +++ b/includes/class-zoninator-api-controller.php @@ -73,25 +73,11 @@ function setup() { ->permissions( 'get_zone_posts_permissions_check' ) ->args( '_get_zone_id_param' ) ) - ->add_action( $this->action( 'create', 'add_post_to_zone' ) - ->permissions( 'add_post_to_zone_permissions_check' ) - ->args( '_get_zone_post_rest_route_params' ) - ) - ->add_action( $this->action( 'update', 'add_post_to_zone' ) - ->permissions( 'add_post_to_zone_permissions_check' ) + ->add_action( $this->action( 'update', 'update_zone_posts' ) + ->permissions( 'update_zone_permissions_check' ) ->args( '_get_zone_post_rest_route_params' ) ); - $this->add_route( 'zones/(?P[\d]+)/posts/(?P\d+)' ) - ->add_action( $this->action( 'delete', 'remove_post_from_zone' ) - ->permissions( 'remove_post_from_zone_permissions_check' ) - ->args( '_get_zone_post_rest_route_params' ) ); - - $this->add_route( 'zones/(?P[\d]+)/posts/order' ) - ->add_action( $this->action( 'update', 'reorder_posts' ) - ->permissions( 'update_zone_permissions_check' ) - ->args( '_get_zone_id_param' ) ); - $this->add_route( 'zones/(?P[\d]+)/lock' ) ->add_action( $this->action( 'update', 'zone_update_lock' ) ->permissions( 'update_zone_permissions_check' ) @@ -153,140 +139,56 @@ function create_zone( $request ) { } /** - * Add a post to zone + * Get zone posts * * @param WP_REST_Request $request Full data about the request. * @return WP_Error|WP_REST_Response */ - function add_post_to_zone( $request ) { + public function get_zone_posts( $request ) { $zone_id = $this->_get_param( $request, 'zone_id', 0, 'absint' ); - $post_id = $this->_get_param( $request, 'post_id', 0, 'absint' ); - - $post = get_post( $post_id ); - - if ( ! $post ) { - $data = array( - 'message' => $this->translations[ self::INVALID_POST_ID ], - ); - return $this->bad_request( $data ); - } - - $zone = $this->instance->get_zone( $zone_id ); - if ( ! $zone ) { - - return $this->not_found( $this->translations[ self::INVALID_POST_ID ] ); + if ( empty( $zone_id ) || false === $this->instance->get_zone( $zone_id ) ) { + return $this->not_found( $this->translations[ self::INVALID_ZONE_ID ] ); } - $result = $this->instance->add_zone_posts( $zone_id, $post, true ); + $results = $this->instance->get_zone_feed( $zone_id ); - if ( is_wp_error( $result ) ) { - return $this->respond( $result, 500 ); + if ( is_wp_error( $results ) ) { + return $this->_bad_request( self::ZONE_FEED_ERROR, $results->get_error_message() ); } - $content = $this->instance->get_admin_zone_post( $post, $zone ); - - $response_data = array( - 'zone_id' => $zone_id, - 'content' => $content, - ); - - return WP_REST_Server::CREATABLE === $request->get_method() ? - $this->created( $response_data ) : - $this->ok( $response_data ); + return new WP_REST_Response( $results, 200 ); } /** - * Delete one item from the collection. + * Sets the posts for a zone * - * @param WP_REST_Request $request The Request. + * @param WP_REST_Request $request Full data about the request.] * @return WP_Error|WP_REST_Response */ - function remove_post_from_zone( $request ) { + function update_zone_posts( $request ) { $zone_id = $this->_get_param( $request, 'zone_id', 0, 'absint' ); - $post_id = $this->_get_param( $request, 'post_id', 0, 'absint' ); + $post_ids = $this->_get_param( $request, 'post_ids', array() ); - if ( empty( $zone_id ) ) { - $data = array( - 'message' => $this->translations[ self::ZONE_ID_POST_ID_REQUIRED ], - ); - return $this->bad_request( $data ); + if ( ! $this->instance->get_zone( $zone_id ) ) { + return $this->not_found( $this->translations[ self::INVALID_ZONE_ID ] ); } - if ( false === $this->instance->get_zone( $zone_id ) ) { - return $this->not_found( $this->translations[ self::ZONE_NOT_FOUND ] ); - } + $posts = array_map( 'get_post', $post_ids ); - if ( empty( $post_id ) ) { - $data = array( - 'message' => $this->translations[ self::ZONE_ID_POST_ID_REQUIRED ], - ); - return $this->bad_request( $data ); - } - - $post = get_post( $post_id ); - if ( empty( $post ) ) { - $data = array( + if ( count( $posts ) !== count( array_filter( $posts ) ) ) { + return $this->bad_request( array( 'message' => $this->translations[ self::INVALID_POST_ID ], - ); - return $this->bad_request( $data ); + ) ); } - $result = $this->instance->remove_zone_posts( $zone_id, $post_id ); + $result = $this->instance->add_zone_posts( $zone_id, $posts ); if ( is_wp_error( $result ) ) { return $this->respond( $result, 500 ); } - $result = array( - 'zone_id' => $zone_id, - 'post_id' => $post_id, - 'content' => '', - 'status' => 200, - ); - return $this->ok( $result ); - } - - /** - * Reorder posts for zone. - * - * @param WP_REST_Request $request Full data about the request. - * @return WP_Error|WP_REST_Response - */ - function reorder_posts( $request ) { - $zone_id = $this->_get_param( $request, 'zone_id', 0, 'absint' ); - $post_ids = (array) $this->_get_param( $request, 'posts', array(), 'absint' ); - - if ( ! $zone_id ) { - return $this->_bad_request( self::ZONE_ID_POST_IDS_REQUIRED, __( 'post ids and zone id required', 'zoninator') ); - } - - if ( false === $this->instance->get_zone( $zone_id ) ) { - return $this->not_found( $this->translations[ self::ZONE_NOT_FOUND ] ); - } - - if ( empty( $post_ids ) ) { - return $this->_bad_request( self::ZONE_ID_POST_IDS_REQUIRED, __( 'post ids and zone id required', 'zoninator' ) ); - } - - $result = $this->instance->add_zone_posts( $zone_id, $post_ids, false ); - - if (is_wp_error($result)) { - $status = 0; - $http_status = 500; - $content = $result->get_error_message(); - } else { - $status = 1; - $http_status = 200; - $content = ''; - } - - return new WP_REST_Response( array( - 'zone_id' => $zone_id, - 'post_ids' => $post_ids, - 'content' => $content, - 'status' => $status, - ), $http_status ); + return $this->ok(); } /** @@ -383,28 +285,6 @@ function search_posts( $request ) { return new WP_REST_Response( $stripped_posts, 200 ); } - /** - * Get zone posts - * - * @param WP_REST_Request $request Full data about the request. - * @return WP_Error|WP_REST_Response - */ - public function get_zone_posts( $request ) { - $zone_id = $this->_get_param( $request, 'zone_id', 0, 'absint' ); - - if ( empty( $zone_id ) || false === $this->instance->get_zone( $zone_id ) ) { - return $this->not_found( $this->translations[ self::INVALID_ZONE_ID ] ); - } - - $results = $this->instance->get_zone_feed( $zone_id ); - - if ( is_wp_error( $results ) ) { - return $this->_bad_request( self::ZONE_FEED_ERROR, $results->get_error_message() ); - } - - return new WP_REST_Response( $results, 200 ); - } - /** * Get recent posts, excluding the ones that are already part of the zone provided * Recent posts can be filtered by category and date @@ -519,17 +399,6 @@ public function remove_post_from_zone_permissions_check( $request ) { return $this->_permissions_check( 'update', $zone_id ); } - /** - * Check if a given request has access to add a post in a zone. - * - * @param WP_REST_Request $request Full data about the request. - * @return WP_Error|bool - */ - public function add_post_to_zone_permissions_check( $request ) { - $zone_id = $this->_get_param( $request, 'zone_id', 0, 'absint' ); - return $this->_permissions_check( 'update', $zone_id ); - } - /** * Check if a given request has access to update a zone. * @@ -546,6 +415,10 @@ public function is_numeric( $item ) { return is_numeric( $item ); } + public function is_numeric_array( $items ) { + return count( $items ) === count( array_filter( $items, 'is_numeric') ); + } + public function strip_slashes( $item ) { // see https://github.com/WP-API/WP-API/issues/1520 on why we do not use stripslashes directly return stripslashes( $item ); @@ -611,13 +484,13 @@ public function _get_zone_id_param() { public function _get_zone_post_rest_route_params() { $zone_params = $this->_get_zone_id_param(); - return array_merge(array( - 'post_id' => array( - 'type' => 'integer', - 'validate_callback' => array( $this, 'is_numeric' ), + return array_merge( array( + 'post_ids' => array( + 'type' => 'array', + 'validate_callback' => array( $this, 'is_numeric_array' ), 'required' => true - ) - ), $zone_params); + ), + ), $zone_params ); } public function _params_for_get_recent_posts() diff --git a/tests/unit/class-zoninator-api-controller-test.php b/tests/unit/class-zoninator-api-controller-test.php index a7c0bca..a0324cb 100644 --- a/tests/unit/class-zoninator-api-controller-test.php +++ b/tests/unit/class-zoninator-api-controller-test.php @@ -215,58 +215,61 @@ function test_create_zone_fail_if_invalid_data() { 'description' => 'No slug provided.' ) ); $this->assertResponseStatus( $response, 400 ); - } + } /** - * T test_add_post_to_zone_responds_with_created_when_method_post + * T test_update_zone_posts_responds_with_ok_when_method_put * * @throws Exception E. */ - function test_add_post_to_zone_responds_with_created_when_method_post() { + function test_update_zone_posts_responds_with_success_when_method_put() { $this->login_as_admin(); $post_id = $this->_insert_a_post(); - $zone_id = $this->create_a_zone( 'the-zone-add-post-1', 'The Zone Add Post one' ); - $response = $this->post( '/zoninator/v1/zones/' . $zone_id . '/posts', array( - 'post_id' => $post_id, + $zone_id = $this->create_a_zone( 'test-zone', 'Test Zone' ); + $response = $this->put( '/zoninator/v1/zones/' . $zone_id . '/posts', array( + 'post_ids' => array( $post_id ), ) ); - $this->assertResponseStatus( $response, 201 ); + $this->assertResponseStatus( $response, 200 ); } /** - * T test_add_post_to_zone_responds_with_created_when_method_put + * T test_update_zone_posts_responds_with_not_found_if_zone_not_exist * * @throws Exception E. */ - function test_add_post_to_zone_responds_with_success_when_method_put() { + function test_update_zone_posts_responds_with_not_found_if_zone_not_exist() { $this->login_as_admin(); $post_id = $this->_insert_a_post(); - $zone_id = $this->create_a_zone( 'the-zone-add-post-1', 'The Zone Add Post one' ); - $response = $this->put( '/zoninator/v1/zones/' . $zone_id . '/posts', array( - 'post_id' => $post_id, + $response = $this->put( '/zoninator/v1/zones/666666/posts', array( + 'post_ids' => array( $post_id ), ) ); - $this->assertResponseStatus( $response, 200 ); + $this->assertResponseStatus( $response, 404 ); } /** - * T test_add_post_to_zone_respond_not_found_if_zone_not_exists + * T test_update_zone_posts_fails_if_invalid_data_format * * @throws Exception E. */ - function test_add_post_to_zone_respond_not_found_if_zone_not_exists() { + function test_update_zone_posts_fails_if_invalid_data() { + $this->login_as_admin(); $post_id = $this->_insert_a_post(); - $response = $this->put( '/zoninator/v1/zones/666666/posts', array( - 'post_id' => $post_id, - ) ); - $this->assertResponseStatus( $response, 404 ); + $zone_id = $this->create_a_zone( 'test-zone', 'Test Zone' ); + $response = $this->put( '/zoninator/v1/zones/' . $zone_id . '/posts', array() ); + $this->assertResponseStatus( $response, 400 ); } /** - * Test test_add_post_to_zone_fail_if_invalid_post + * T test_update_zone_posts_fails_if_invalid_post_id + * + * @throws Exception E. */ - function test_add_post_to_zone_fail_if_invalid_post() { - $zone_id = $this->add_a_zone( 'zone-test_add_post_to_zone_fail_if_invalid_post' ); - $response = $this->post( '/zoninator/v1/zones/' . $zone_id . '/posts', array( - 'post_id' => 666666, + function test_update_zone_posts_fails_if_invalid_data() { + $this->login_as_admin(); + $post_id = $this->_insert_a_post(); + $zone_id = $this->create_a_zone( 'test-zone', 'Test Zone' ); + $response = $this->put( '/zoninator/v1/zones/' . $zone_id . '/posts', array( + 'post_ids' => array( 123456789 ), ) ); $this->assertResponseStatus( $response, 400 ); } @@ -321,123 +324,6 @@ function test_get_zone_posts_fail_when_no_posts_in_zone() { $this->assertResponseStatus( $response, 400 ); } - /** - * Test test_remove_post_from_zone_bad_request_if_invalid_post_id - */ - function test_remove_post_from_zone_bad_request_if_invalid_post_id() { - $zone_id = $this->add_a_zone( 'zone-test_remove_post_from_zone_bad_request_if_invalid_post_id' ); - $response = $this->delete( '/zoninator/v1/zones/' . $zone_id . '/posts/0' ); - $this->assertResponseStatus( $response, 400 ); - } - - /** - * Test test_remove_post_from_zone_not_found_if_no_zone_id - */ - function test_remove_post_from_zone_not_found_if_no_zone_id() { - $response = $this->delete( '/zoninator/v1/zones/121212/posts/' ); - $this->assertResponseStatus( $response, 404 ); - } - - /** - * Test test_remove_post_from_zone_succeed_if_successful - */ - function test_remove_post_from_zone_succeed_if_successful() { - $this->login_as_admin(); - $zone_id = $this->add_a_zone( 'zone-test_remove_post_from_zone_succeed_if_successful' ); - self::factory()->post->create_many( 5 ); - $query = new WP_Query(); - $posts = $query->query( array() ); - foreach ( $posts as $post ) { - $response = $this->post( '/zoninator/v1/zones/' . $zone_id . '/posts', array( - 'post_id' => $post->ID, - ) ); - $this->assertResponseStatus( $response, 201 ); - } - - $response = $this->get( '/zoninator/v1/zones/' . $zone_id . '/posts' ); - $this->assertResponseStatus( $response, 200 ); - $data = $response->get_data(); - $this->assertSame( count( $posts ), count( $data ) ); - $first_post = $data[0]; - $response = $this->delete( '/zoninator/v1/zones/' . $zone_id . '/posts/' . $first_post->ID ); - $this->assertResponseStatus( $response, 200 ); - $response = $this->get( '/zoninator/v1/zones/' . $zone_id . '/posts' ); - $this->assertResponseStatus( $response, 200 ); - $data = $response->get_data(); - $this->assertSame( count( $posts ) - 1, count( $data ) ); - $ids = wp_list_pluck( $data, 'ID' ); - $this->assertTrue( ! in_array( $first_post->ID, $ids, true ) ); - } - - /** - * Test test_reorder_posts_on_zone_return_WP_Error_if_post_ids_not_present - */ - function test_reorder_posts_on_zone_return_WP_Error_if_post_ids_not_present() { - $this->login_as_admin(); - $zone_id = $this->add_a_zone( 'zone-test_reorder_posts_on_zone_return_WP_Error_if_post_ids_not_present' ); - self::factory()->post->create_many( 5 ); - $query = new WP_Query(); - $posts = $query->query( array() ); - foreach ( $posts as $post ) { - $response = $this->post( '/zoninator/v1/zones/' . $zone_id . '/posts', array( - 'post_id' => $post->ID, - ) ); - $this->assertResponseStatus( $response, 201 ); - } - - $response = $this->get( '/zoninator/v1/zones/' . $zone_id . '/posts' ); - $this->assertResponseStatus( $response, 200 ); - $data = $response->get_data(); - $ids = wp_list_pluck( $data, 'ID' ); - shuffle( $ids ); - $request_data = array(); - $response = $this->put( '/zoninator/v1/zones/' . $zone_id . '/posts/order', $request_data ); - $this->assertResponseStatus( $response, 400 ); - } - - /** - * Test test_reorder_posts_on_zone_success - */ - function test_reorder_posts_on_zone_success() { - $this->login_as_admin(); - $zone_id = $this->add_a_zone( 'zone-test_reorder_posts_on_zone_success' ); - self::factory()->post->create_many( 5 ); - $query = new WP_Query(); - $posts = $query->query( array() ); - foreach ( $posts as $post ) { - $response = $this->post( '/zoninator/v1/zones/' . $zone_id . '/posts', array( - 'post_id' => $post->ID, - ) ); - $this->assertResponseStatus( $response, 201 ); - } - - $response = $this->get( '/zoninator/v1/zones/' . $zone_id . '/posts' ); - $this->assertResponseStatus( $response, 200 ); - $data = $response->get_data(); - $ids = wp_list_pluck( $data, 'ID' ); - shuffle( $ids ); - $request_data = array( - 'posts' => $ids, - ); - - $response = $this->put( '/zoninator/v1/zones/' . $zone_id . '/posts/order', $request_data ); - $this->assertResponseStatus( $response, 200 ); - - $response = $this->get( '/zoninator/v1/zones/' . $zone_id . '/posts' ); - $this->assertResponseStatus( $response, 200 ); - $data = $response->get_data(); - $reordered_ids = wp_list_pluck( $data, 'ID' ); - - $this->assertEquals( $ids, $reordered_ids ); - } - - /** - * Test test_reorder_posts_on_zone_return_WP_Error_if_zone_id_not_present - */ - function test_reorder_posts_on_zone_return_WP_Error_if_zone_id_not_present() { - $response = $this->put( '/zoninator/v1/zones/123123/posts/order', array() ); - $this->assertResponseStatus( $response, 404 ); - } // // function test_zone_update_lock_200() // { From 5f937ac0c3a07e492b3eeec4b921704a790dcf3d Mon Sep 17 00:00:00 2001 From: Kuba Birecki Date: Mon, 4 Sep 2017 12:06:42 +0200 Subject: [PATCH 17/31] Add update & delete zone endpoints --- includes/class-zoninator-api-controller.php | 99 +++++++++++++++++++ .../class-zoninator-api-controller-test.php | 50 ++++++++++ 2 files changed, 149 insertions(+) diff --git a/includes/class-zoninator-api-controller.php b/includes/class-zoninator-api-controller.php index fcd48be..87746be 100644 --- a/includes/class-zoninator-api-controller.php +++ b/includes/class-zoninator-api-controller.php @@ -68,6 +68,17 @@ function setup() { ->args( '_params_for_create_zone' ) ); + $this->add_route( 'zones/(?P[\d]+)' ) + ->add_action( + $this->action( 'update', 'update_zone' ) + ->permissions( 'update_zone_permissions_check' ) + ->args( '_params_for_update_zone' ) + ) + ->add_action( + $this->action( 'delete', 'delete_zone' ) + ->permissions( 'update_zone_permissions_check' ) + ); + $this->add_route( 'zones/(?P[\d]+)/posts' ) ->add_action( $this->action( 'index', 'get_zone_posts' ) ->permissions( 'get_zone_posts_permissions_check' ) @@ -138,6 +149,74 @@ function create_zone( $request ) { return $this->created( $this->_filter_zone_properties( $zone ) ); } + /** + * Update zone details + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|WP_REST_Response + */ + function update_zone( $request ) { + $zone_id = $this->_get_param( $request, 'zone_id', 0, 'absint' ); + $name = $this->_get_param( $request, 'name', '' ); + $slug = $this->_get_param( $request, 'slug', '' ); + $description = $this->_get_param( $request, 'description', '', 'strip_tags' ); + + $zone = $this->instance->get_zone( $zone_id ); + $update_params = array(); + + if ( ! $zone ) { + return $this->not_found( $this->translations[ self::INVALID_ZONE_ID ] ); + } + + if ( $name ) { + $update_params[ 'name' ] = $name; + } + + if ( $slug ) { + $update_params[ 'slug' ] = $slug; + } + + if ( $description ) { + $update_params[ 'details' ] = array( 'description' => $description ); + } + + $result = $this->instance->update_zone( $zone, $update_params ); + + if ( is_wp_error( $result ) ) { + return $this->bad_request( array( + 'message' => $result->get_error_message(), + ) ); + } + + return $this->ok(); + } + + /** + * Delete a zone + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|WP_REST_Response + */ + function delete_zone( $request ) { + $zone_id = $this->_get_param( $request, 'zone_id', 0, 'absint' ); + + $zone = $this->instance->get_zone( $zone_id ); + + if ( ! $zone ) { + return $this->not_found( $this->translations[ self::INVALID_ZONE_ID ] ); + } + + $result = $this->instance->delete_zone( $zone ); + + if ( is_wp_error( $result ) ) { + return $this->bad_request( array( + 'message' => $result->get_error_message(), + ) ); + } + + return $this->ok(); + } + /** * Get zone posts * @@ -471,6 +550,26 @@ public function _params_for_create_zone() { ); } + public function _params_for_update_zone() { + return array( + 'name' => array( + 'type' => 'string', + 'sanitize_callback' => array( $this, 'strip_slashes' ), + 'required' => false + ), + 'slug' => array( + 'type' => 'string', + 'sanitize_callback' => array( $this, 'strip_slashes' ), + 'required' => false, + ), + 'description' => array( + 'type' => 'string', + 'sanitize_callback' => array( $this, 'strip_tags' ), + 'required' => false, + ) + ); + } + public function _get_zone_id_param() { return array( 'zone_id' => array( diff --git a/tests/unit/class-zoninator-api-controller-test.php b/tests/unit/class-zoninator-api-controller-test.php index a0324cb..3c8534e 100644 --- a/tests/unit/class-zoninator-api-controller-test.php +++ b/tests/unit/class-zoninator-api-controller-test.php @@ -217,6 +217,56 @@ function test_create_zone_fail_if_invalid_data() { $this->assertResponseStatus( $response, 400 ); } + /** + * T test_update_zone_responds_with_success_when_method_put + * + * @throws Exception E. + */ + function test_update_zone_responds_with_success_when_method_put() { + $this->login_as_admin(); + $zone_id = $this->create_a_zone( 'test-update-zone', 'Test Zone' ); + $response = $this->put( '/zoninator/v1/zones/' . $zone_id, array( + 'name' => 'Other test zone', + ) ); + $this->assertResponseStatus( $response, 200 ); + } + + /** + * T test_update_zone_responds_with_not_found_if_zone_not_exist + * + * @throws Exception E. + */ + function test_update_zone_responds_with_not_found_if_zone_not_exist() { + $this->login_as_admin(); + $response = $this->put( '/zoninator/v1/zones/666666', array( + 'name' => 'Other test zone', + ) ); + $this->assertResponseStatus( $response, 404 ); + } + + /** + * T test_delete_zone_responds_with_success_when_method_delete + * + * @throws Exception E. + */ + function test_delete_zone_responds_with_success_when_method_delete() { + $this->login_as_admin(); + $zone_id = $this->create_a_zone( 'test-update-zone', 'Test Zone' ); + $response = $this->delete( '/zoninator/v1/zones/' . $zone_id ); + $this->assertResponseStatus( $response, 200 ); + } + + /** + * T test_delete_zone_responds_with_not_found_if_zone_not_exist + * + * @throws Exception E. + */ + function test_delete_zone_responds_with_not_found_if_zone_not_exist() { + $this->login_as_admin(); + $response = $this->delete( '/zoninator/v1/zones/666666' ); + $this->assertResponseStatus( $response, 404 ); + } + /** * T test_update_zone_posts_responds_with_ok_when_method_put * From 53dc66e7a26b3a7ea1f8e7e4798f7f9db82ddf37 Mon Sep 17 00:00:00 2001 From: Kuba Birecki Date: Mon, 4 Sep 2017 20:57:09 +0200 Subject: [PATCH 18/31] Prevent 'null' responses --- includes/class-zoninator-api-controller.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/class-zoninator-api-controller.php b/includes/class-zoninator-api-controller.php index 87746be..3057566 100644 --- a/includes/class-zoninator-api-controller.php +++ b/includes/class-zoninator-api-controller.php @@ -188,7 +188,7 @@ function update_zone( $request ) { ) ); } - return $this->ok(); + return $this->ok( array( 'success' => true ) ); } /** @@ -214,7 +214,7 @@ function delete_zone( $request ) { ) ); } - return $this->ok(); + return $this->ok( array( 'success' => true ) ); } /** @@ -267,7 +267,7 @@ function update_zone_posts( $request ) { return $this->respond( $result, 500 ); } - return $this->ok(); + return $this->ok( array( 'success' => true ) ); } /** From c535fa138fe75c9296b58fd6710d6a8838286b07 Mon Sep 17 00:00:00 2001 From: Kuba Birecki Date: Tue, 5 Sep 2017 19:13:32 +0200 Subject: [PATCH 19/31] Don't throw an error when a zone has no posts --- zoninator.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/zoninator.php b/zoninator.php index 7083e39..e831fb8 100644 --- a/zoninator.php +++ b/zoninator.php @@ -1656,11 +1656,6 @@ function get_zone_feed( $zone_slug_or_id ) { } $results = $this->get_zone_posts( $zone_id, apply_filters( 'zoninator_json_feed_fields', array(), $zone_slug_or_id ) ); - - if ( empty( $results ) ) { - return new WP_Error( 'no-zone-posts-found', __( 'No zone posts found', 'zoninator' ) ); - } - $filtered_results = $this->filter_zone_feed_fields( $results ); return apply_filters( 'zoninator_json_feed_results', $filtered_results, $zone_slug_or_id ); From 959915a7570bca1f736dc961b8b7f7b4f1050ac5 Mon Sep 17 00:00:00 2001 From: Kuba Birecki Date: Wed, 6 Sep 2017 13:06:23 +0200 Subject: [PATCH 20/31] Fix unit tests --- includes/class-zoninator-api-controller.php | 3 ++- .../class-zoninator-api-controller-test.php | 24 +++++++++---------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/includes/class-zoninator-api-controller.php b/includes/class-zoninator-api-controller.php index 3057566..02dd018 100644 --- a/includes/class-zoninator-api-controller.php +++ b/includes/class-zoninator-api-controller.php @@ -587,7 +587,8 @@ public function _get_zone_post_rest_route_params() { 'post_ids' => array( 'type' => 'array', 'validate_callback' => array( $this, 'is_numeric_array' ), - 'required' => true + 'required' => true, + 'items' => array( 'type' => 'integer' ), ), ), $zone_params ); } diff --git a/tests/unit/class-zoninator-api-controller-test.php b/tests/unit/class-zoninator-api-controller-test.php index 3c8534e..efecc6f 100644 --- a/tests/unit/class-zoninator-api-controller-test.php +++ b/tests/unit/class-zoninator-api-controller-test.php @@ -314,7 +314,7 @@ function test_update_zone_posts_fails_if_invalid_data() { * * @throws Exception E. */ - function test_update_zone_posts_fails_if_invalid_data() { + function test_update_zone_posts_fails_if_invalid_post_id() { $this->login_as_admin(); $post_id = $this->_insert_a_post(); $zone_id = $this->create_a_zone( 'test-zone', 'Test Zone' ); @@ -336,18 +336,18 @@ function test_get_zone_posts_success_when_valid_zone_and_posts() { $posts = $query->query( array() ); $post = $posts[0]; $zone_id = $this->add_a_zone(); - $response = $this->post( '/zoninator/v1/zones/' . $zone_id . '/posts', array( - 'post_id' => $post->ID, + $response = $this->put( '/zoninator/v1/zones/' . $zone_id . '/posts', array( + 'post_ids' => array( $post->ID ), ) ); - $this->assertResponseStatus( $response, 201 ); + $this->assertResponseStatus( $response, 200 ); $response = $this->get( '/zoninator/v1/zones/' . $zone_id . '/posts' ); $this->assertResponseStatus( $response, 200 ); } /** - * Test test_get_zone_posts_not_found_when_invalid_zone + * Test test_get_zone_posts_success_when_no_posts_in_zone */ - function test_get_zone_posts_not_found_when_invalid_zone() { + function test_get_zone_posts_success_when_no_posts_in_zone() { $term_factory = new WP_UnitTest_Factory_For_Term( null, Zoninator()->zone_taxonomy ); $zone_id = $term_factory->create_object( array( 'name' => 'The Zone Add Post one', @@ -355,14 +355,14 @@ function test_get_zone_posts_not_found_when_invalid_zone() { 'slug' => 'zone-2', ) ); - $response = $this->get( '/zoninator/v1/zones/' . ( $zone_id + 3 ) ); - $this->assertResponseStatus( $response, 404 ); + $response = $this->get( '/zoninator/v1/zones/' . $zone_id . '/posts' ); + $this->assertResponseStatus( $response, 200 ); } /** - * Test test_get_zone_posts_fail_when_no_posts_in_zone + * Test test_get_zone_posts_not_found_when_invalid_zone */ - function test_get_zone_posts_fail_when_no_posts_in_zone() { + function test_get_zone_posts_not_found_when_invalid_zone() { $term_factory = new WP_UnitTest_Factory_For_Term( null, Zoninator()->zone_taxonomy ); $zone_id = $term_factory->create_object( array( 'name' => 'The Zone Add Post one', @@ -370,8 +370,8 @@ function test_get_zone_posts_fail_when_no_posts_in_zone() { 'slug' => 'zone-2', ) ); - $response = $this->get( '/zoninator/v1/zones/' . $zone_id . '/posts' ); - $this->assertResponseStatus( $response, 400 ); + $response = $this->get( '/zoninator/v1/zones/' . ( $zone_id + 3 ) ); + $this->assertResponseStatus( $response, 404 ); } // From a292ee63210b1b787518ec39f982a0a0d801352a Mon Sep 17 00:00:00 2001 From: Kuba Birecki Date: Thu, 7 Sep 2017 10:47:09 +0200 Subject: [PATCH 21/31] Remove unused REST endpoints --- includes/class-zoninator-api-controller.php | 268 +------------------- 1 file changed, 8 insertions(+), 260 deletions(-) diff --git a/includes/class-zoninator-api-controller.php b/includes/class-zoninator-api-controller.php index 02dd018..79519db 100644 --- a/includes/class-zoninator-api-controller.php +++ b/includes/class-zoninator-api-controller.php @@ -80,27 +80,16 @@ function setup() { ); $this->add_route( 'zones/(?P[\d]+)/posts' ) - ->add_action( $this->action( 'index', 'get_zone_posts' ) - ->permissions( 'get_zone_posts_permissions_check' ) - ->args( '_get_zone_id_param' ) + ->add_action( + $this->action( 'index', 'get_zone_posts' ) + ->permissions( 'get_zone_posts_permissions_check' ) + ->args( '_get_zone_id_param' ) ) - ->add_action( $this->action( 'update', 'update_zone_posts' ) - ->permissions( 'update_zone_permissions_check' ) - ->args( '_get_zone_post_rest_route_params' ) + ->add_action( + $this->action( 'update', 'update_zone_posts' ) + ->permissions( 'update_zone_permissions_check' ) + ->args( '_get_zone_post_rest_route_params' ) ); - - $this->add_route( 'zones/(?P[\d]+)/lock' ) - ->add_action( $this->action( 'update', 'zone_update_lock' ) - ->permissions( 'update_zone_permissions_check' ) - ->args( '_get_zone_id_param' ) ); - - $this->add_route( 'posts/search' ) - ->add_action( $this->action( 'index', 'search_posts' ) - ->args( '_params_for_search_posts' ) ); - - $this->add_route( 'posts/recent' ) - ->add_action( $this->action( 'index', 'get_recent_posts' ) - ->args( '_params_for_get_recent_posts' ) ); } /** @@ -270,173 +259,6 @@ function update_zone_posts( $request ) { return $this->ok( array( 'success' => true ) ); } - /** - * Update the zone's lock - * - * @param WP_REST_Request $request Full data about the request. - * @return WP_Error|WP_REST_Response - */ - function zone_update_lock( $request ) { - $zone_id = $this->_get_param( $request, 'zone_id', 0, 'absint' ); - - if (! $zone_id ) { - return $this->_bad_request(self::ZONE_ID_REQUIRED, __('zone id required', 'zoninator')); - } - - if ( ! $this->instance->is_zone_locked( $zone_id ) ) { - $this->instance->lock_zone( $zone_id ); - return new WP_REST_Response(array( - 'zone_id' => $zone_id, - 'status' => 1, - ), 200); - } - - return new WP_REST_Response(array( - 'zone_id' => $zone_id, - 'status' => 0), 400); - } - - /** - * Search posts for "term" - * - * @param WP_REST_Request $request Full data about the request. - * @return WP_Error|WP_REST_Response - */ - function search_posts( $request ) { - $search_filter_definition = $this->environment()->model( 'Zoninator_Api_Filter_Search' ); - $search_filter = $search_filter_definition->new_from_array( $request->get_params() ); - - $validation_error = $search_filter->validate(); - if ( is_wp_error( $validation_error ) ) { - return $this->respond( $validation_error, 400 ); - } - - $filter_cat = $search_filter->get( 'cat' ); - $filter_date = $search_filter->get( 'date' ); - - $post_types = $this->instance->get_supported_post_types(); - $limit = $this->_get_param( $request, 'limit', $this->instance->posts_per_page ); - - if ( 0 >= $limit ) { - $limit = $this->instance->posts_per_page; - } - - $exclude = (array)$search_filter->get( 'exclude' ); - - $args = apply_filters('zoninator_search_args', array( - 's' => $search_filter->get( 'term' ), - 'post__not_in' => $exclude, - 'posts_per_page' => $limit, - 'post_type' => $post_types, - 'post_status' => array('publish', 'future'), - 'order' => 'DESC', - 'orderby' => 'post_date', - 'suppress_filters' => true, - )); - - if ( $this->instance->_validate_category_filter( $filter_cat ) ) { - $args['cat'] = $filter_cat; - } - - if ( $this->instance->_validate_date_filter( $filter_date ) ) { - $filter_date_parts = explode( '-', $filter_date ); - $args['year'] = $filter_date_parts[0]; - $args['monthnum'] = $filter_date_parts[1]; - $args['day'] = $filter_date_parts[2]; - } - - $query = new WP_Query($args); - - $stripped_posts = array(); - - if ( $query->have_posts() ) { - foreach ( $query->posts as $post ) { - $stripped_posts[] = apply_filters( 'zoninator_search_results_post', array( - 'title' => !empty( $post->post_title ) ? $post->post_title : __( '(no title)', 'zoninator' ), - 'post_id' => $post->ID, - 'date' => get_the_time( get_option( 'date_format' ), $post ), - 'post_type' => $post->post_type, - 'post_status' => $post->post_status, - ), $post ); - } - } - - return new WP_REST_Response( $stripped_posts, 200 ); - } - - /** - * Get recent posts, excluding the ones that are already part of the zone provided - * Recent posts can be filtered by category and date - * - * @param WP_REST_Request $request - * @return WP_Error|WP_REST_Response - */ - public function get_recent_posts( $request ) { - $cat = $this->_get_param( $request, 'cat', '', 'absint' ); - $date = $this->_get_param( $request, 'date', '', 'striptags' ); - $zone_id = $this->_get_param( $request, 'zone_id', 0, 'absint' ); - - $limit = $this->instance->posts_per_page; - $post_types = $this->instance->get_supported_post_types(); - $zone_posts = $this->instance->get_zone_posts( $zone_id ); - $zone_post_ids = wp_list_pluck( $zone_posts, 'ID' ); - - $http_status = 200; - - if ( is_wp_error( $zone_posts ) ) { - $status = 0; - $content = $zone_posts->get_error_message(); - $http_status = 500; - } else { - $args = apply_filters( 'zoninator_recent_posts_args', array( - 'posts_per_page' => $limit, - 'order' => 'DESC', - 'orderby' => 'post_date', - 'post_type' => $post_types, - 'ignore_sticky_posts' => true, - 'post_status' => array( 'publish', 'future' ), - 'post__not_in' => $zone_post_ids, - ) ); - - if ( $this->instance->_validate_category_filter( $cat ) ) { - $args['cat'] = $cat; - } - - if ( $this->instance->_validate_date_filter( $date ) ) { - $filter_date_parts = explode( '-', $date ); - $args['year'] = $filter_date_parts[0]; - $args['monthnum'] = $filter_date_parts[1]; - $args['day'] = $filter_date_parts[2]; - } - - $content = ''; - $recent_posts = get_posts( $args ); - foreach ( $recent_posts as $post ) { - $content .= sprintf('', $post->ID, get_the_title($post->ID) . ' (' . $post->post_status . ')'); - } - - wp_reset_postdata(); - $status = 1; - } - - if ( ! $content ) { - $empty_label = __( 'No results found', 'zoninator' ); - } elseif ( $cat ) { - $empty_label = sprintf(__('Choose post from %s', 'zoninator'), get_the_category_by_ID($cat)); - } else { - $empty_label = __( 'Choose a post', 'zoninator' ); - } - - $content = '' . $content; - - $response = new WP_REST_Response( array( - 'zone_id' => $zone_id, - 'content' => $content, - 'status' => $status ) ); - $response->set_status( $http_status ); - return $response; - } - /** * Check if a given request has access to the zones index. * @@ -467,17 +289,6 @@ public function get_zone_posts_permissions_check( $request ) { return true; } - /** - * Check if a given request has access to remove a post from zone. - * - * @param WP_REST_Request $request Full data about the request. - * @return WP_Error|bool - */ - public function remove_post_from_zone_permissions_check( $request ) { - $zone_id = $this->_get_param( $request, 'zone_id', 0, 'absint' ); - return $this->_permissions_check( 'update', $zone_id ); - } - /** * Check if a given request has access to update a zone. * @@ -593,69 +404,6 @@ public function _get_zone_post_rest_route_params() { ), $zone_params ); } - public function _params_for_get_recent_posts() - { - $zone_params = $this->_get_zone_id_param(); - return array_merge(array( - 'cat' => array( - 'description' => __( 'only recent posts from this category id', 'zoninator' ), - 'type' => 'integer', - 'validate_callback' => array( $this, 'is_numeric' ), - 'sanitize_callback' => 'absint', - 'default' => 0, - 'required' => false - ), - 'date' => array( - 'description' => __( 'only get posts after this date (format YYYY-mm-dd)', 'zoninator' ), - 'type' => 'string', - 'sanitize_callback' => array( $this, 'strip_slashes' ), - 'default' => '', - 'required' => false - ) - ), $zone_params); - } - - public function _params_for_search_posts() { - $search_filter = $this->environment()->model( 'Zoninator_Api_Filter_Search' ); - $schema_converter = new Zoninator_Api_Schema_Converter(); - return $schema_converter->as_args( $search_filter ); -// return array( -// 'term' => array( -// 'description' => __( 'search term', 'zoninator' ), -// 'type' => 'string', -// 'sanitize_callback' => array( $this, 'strip_slashes' ), -// 'default' => '', -// 'required' => true -// ), -// 'cat' => array( -// 'description' => __( 'filter by category', 'zoninator' ), -// 'type' => 'integer', -// 'validate_callback' => array( $this, 'is_numeric' ), -// 'sanitize_callback' => 'absint', -// 'default' => 0, -// 'required' => false -// ), -// 'date' => array( -// 'description' => __( 'only get posts after this date (format YYYY-mm-dd)', 'zoninator' ), -// 'type' => 'string', -// 'sanitize_callback' => array( $this, 'strip_slashes' ), -// 'default' => '', -// 'required' => false -// ), -// 'limit' => array( -// 'description' => __( 'limit results', 'zoninator' ), -// 'type' => 'integer', -// 'sanitize_callback' => 'absint', -// 'default' => $this->instance->posts_per_page, -// 'required' => false -// ), -// 'exclude' => array( -// 'description' => __( 'post_ids to exclude', 'zoninator' ), -// 'required' => false -// ) -// ); - } - public function _filter_zone_properties( $zone ) { $data = $zone->to_array(); From d3b0d2945438bc72d646295b11623743ad4a138f Mon Sep 17 00:00:00 2001 From: Kuba Birecki Date: Thu, 7 Sep 2017 10:51:54 +0200 Subject: [PATCH 22/31] Update tests --- .../class-zoninator-api-controller-test.php | 54 ------------------- 1 file changed, 54 deletions(-) diff --git a/tests/unit/class-zoninator-api-controller-test.php b/tests/unit/class-zoninator-api-controller-test.php index efecc6f..28eb799 100644 --- a/tests/unit/class-zoninator-api-controller-test.php +++ b/tests/unit/class-zoninator-api-controller-test.php @@ -211,7 +211,6 @@ function test_create_zone_responds_with_created_when_method_post() { function test_create_zone_fail_if_invalid_data() { $this->login_as_admin(); $response = $this->post( '/zoninator/v1/zones', array( - 'name' => 'Test zone', 'description' => 'No slug provided.' ) ); $this->assertResponseStatus( $response, 400 ); @@ -374,59 +373,6 @@ function test_get_zone_posts_not_found_when_invalid_zone() { $this->assertResponseStatus( $response, 404 ); } -// -// function test_zone_update_lock_200() -// { -// $this->_zoninator->method( 'is_zone_locked' )->willReturn( false ); -// $request = $this->_create_request( array( 'zone_id' => 3 ) ); -// $response = $this->_controller->zone_update_lock( $request ); -// $this->_assert_response_status($response, 200); -// } -// -// function test_zone_update_lock_400_if_zone_already_locked() -// { -// $this->_zoninator->method( 'is_zone_locked' )->willReturn( true ); -// $request = $this->_create_request( array( 'zone_id' => 3 ) ); -// $response = $this->_controller->zone_update_lock( $request ); -// $this->_assert_response_status($response, 400); -// } -// -// function test_zone_update_error_if_no_zone_id() -// { -// $this->_zoninator->method( 'is_zone_locked' )->willReturn( false ); -// $request = $this->_create_request( array( ) ); -// $response = $this->_controller->zone_update_lock( $request ); -// $this->assertInstanceOf( 'WP_Error', $response ); -// } -// - /** - * Test test_search_posts_error_if_empty_term - */ - function test_search_posts_return_results() { - self::factory()->post->create_many( 5 ); - $query = new WP_Query(); - $posts = $query->query( array() ); - $first = $posts[0]; - $data = array( 'term' => $first->post_title ); - $response = $this->get( '/zoninator/v1/posts/search', $data ); - $this->assertResponseStatus( $response, 200 ); - $data = $response->get_data(); - $first_result = $data[0]; - $this->assertEquals( $first->ID, $first_result['post_id'] ); - } - - /** - * Test test_search_posts_error_if_empty_term - */ - function test_search_posts_error_if_empty_term() { - self::factory()->post->create_many( 5 ); - $query = new WP_Query(); - $posts = $query->query( array() ); - $data = array( 'term' => '' ); - $response = $this->get( '/zoninator/v1/posts/search', $data ); - $this->assertResponseStatus( $response, 400 ); - } - /** * @return int|WP_Error */ From 0fb2d994e9d5a2838c55bf1b2a4edc27f0fef8d8 Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Fri, 8 Sep 2017 18:30:57 +0300 Subject: [PATCH 23/31] migrate to mixtape on npm --- .mixtapefile | 3 - mixtape.json | 4 ++ package.json | 16 ++++++ scripts/build_mixtape.sh | 118 --------------------------------------- 4 files changed, 20 insertions(+), 121 deletions(-) delete mode 100644 .mixtapefile create mode 100644 mixtape.json create mode 100644 package.json delete mode 100755 scripts/build_mixtape.sh diff --git a/.mixtapefile b/.mixtapefile deleted file mode 100644 index 8e2631b..0000000 --- a/.mixtapefile +++ /dev/null @@ -1,3 +0,0 @@ -sha=bc384c9df8b92a347b39c0aacc1d97b17f06a566 -prefix=Zoninator_REST -destination=lib/zoninator_rest diff --git a/mixtape.json b/mixtape.json new file mode 100644 index 0000000..dc6b6f0 --- /dev/null +++ b/mixtape.json @@ -0,0 +1,4 @@ +{ + "prefix": "Zoninator_REST", + "destination": "lib/zoninator_rest" +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..57ed120 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "zoninator", + "title": "Zoninator", + "description": " Curation made easy! Create \"zones\" then add and order your content!", + "version": "0.6.0", + "homepage": "http://vip.wordpress.com", + "license": "GPL-2.0+", + "repository": "automattic/wp-job-manager", + "devDependencies": { + "mixtape": "git+https://github.com/Automattic/mixtape.git#3a8440" + }, + "engines": { + "node": ">=7.0.0", + "npm": ">=3.10.0" + } +} diff --git a/scripts/build_mixtape.sh b/scripts/build_mixtape.sh deleted file mode 100755 index e94423e..0000000 --- a/scripts/build_mixtape.sh +++ /dev/null @@ -1,118 +0,0 @@ -#!/usr/bin/env bash - -set -e; - -SCRIPT_ROOT=`pwd`; -MIXTAPE_TEMP_PATH="$SCRIPT_ROOT/tmp/mt"; -MIXTAPE_REPO="https://github.com/Automattic/mixtape/"; -MIXTAPEFILE_NAME=".mixtapefile"; -MIXTAPE_PATH="${MIXTAPE_PATH-$MIXTAPE_TEMP_PATH}"; - -# Declare all our reusable functions - -show_usage() { - echo "Builds mixtape for development and plugin deployment"; - echo "Note: Requires git"; - echo ""; - echo " ./scripts/build_mixtape.sh"; - echo ""; - echo "Assumes $MIXTAPEFILE_NAME is present at project root, generates a stub otherwise"; -}; - -expect_directory() { - if [ ! -d "$1" ]; then - echo "Not a directory: $1. Exiting" >&2; - exit 1; - fi -}; - -# Check we have git -command -v git >/dev/null 2>&1 || { - echo "No Git found. Exiting." >&2; - show_usage; - exit 1; -}; - -if [ "$MIXTAPE_PATH" == "$MIXTAPE_TEMP_PATH" ]; then - if [ ! -d "$MIXTAPE_PATH" ]; then - mkdir -p "$MIXTAPE_PATH"; - git clone "$MIXTAPE_REPO" "$MIXTAPE_PATH" || { - echo "Error cloning mixtape repo: $MIXTAPE_REPO" >&2; - exit 1; - } - cd "$MIXTAPE_PATH" && git checkout master >/dev/null 2>&1; - if [ "$?" -ne 0 ]; then - echo "Can't run git checkout command on $MIXTAPE_PATH" >&2; - exit 1; - fi - fi - cd "$MIXTAPE_PATH" && git fetch 2>&1; -fi - -cd "$SCRIPT_ROOT"; - -expect_directory "$MIXTAPE_PATH"; - -if [ ! -f "$MIXTAPEFILE_NAME" ]; then - echo "No $MIXTAPEFILE_NAME found. Generating one (using sha from Mixtape HEAD)"; - - echo "sha=$(cd $MIXTAPE_PATH && git rev-parse HEAD)" >> "$MIXTAPEFILE_NAME"; - echo "prefix=YOUR_PREFIX" >> "$MIXTAPEFILE_NAME"; - echo "destination=your/destination" >> "$MIXTAPEFILE_NAME"; - - echo "$MIXTAPEFILE_NAME Generated:"; - echo ""; - cat "$MIXTAPEFILE_NAME"; - echo "Amend it with your prefix, sha and destination and rerun this."; - exit; -fi - -cd "$SCRIPT_ROOT"; - -mt_current_sha="$(cat "$MIXTAPEFILE_NAME" | grep -o 'sha=[^"]*' | sed 's/sha=//')"; -mt_current_prefix="$(cat "$MIXTAPEFILE_NAME" | grep -o 'prefix=[^"]*' | sed 's/prefix=//')"; -mt_current_destination="$(pwd)/$(cat "$MIXTAPEFILE_NAME" | grep -o 'destination=[^"]*' | sed 's/destination=//')"; - -echo "============= Building Mixtape ============="; -echo ""; -echo "SHA = $mt_current_sha"; -echo "PREFIX = $mt_current_prefix"; -echo "DESTINATION = $mt_current_destination"; -echo ""; - -if [ ! -d "$mt_current_destination" ]; then - mkdir -p "$mt_current_destination" -fi - -expect_directory "$mt_current_destination"; - -cd $MIXTAPE_PATH; -mt_repo_current_sha="$(git rev-parse HEAD)"; - -if [ "$mt_repo_current_sha" != "$mt_current_sha" ]; then - echo "Dir"; - git checkout "$mt_current_sha" 2>&1; - if [ $? -ne 0 ]; then - echo "Git checkout error" >&2; - exit 1; - fi -fi - -git diff-index --quiet --cached HEAD >/dev/null 2>&1; - -if [ $? -ne 0 ]; then - echo "Repository (at $MIXTAPE_PATH) is dirty. Please commit or stash the changes. Exiting." >&2; - exit 1; -fi - -echo "Running project script from $MIXTAPE_PATH" -sh "$MIXTAPE_PATH/scripts/new_project.sh" "$mt_current_prefix" "$mt_current_destination"; - -if [ $? -ne 0 ]; then - echo "Something went wrong with the file generation, Exiting" >&2; - git checkout "$mt_repo_current_sha" >/dev/null 2>&1; - exit 1; -else - echo "Generation done!"; - git checkout "$mt_repo_current_sha" >/dev/null 2>&1; -fi From b09191f22fd8d558f6f51f4ee501c759ae4bd244 Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Fri, 8 Sep 2017 18:33:30 +0300 Subject: [PATCH 24/31] change signatures after upgrading to latest mixtape --- includes/class-zoninator-api-controller.php | 6 +++--- includes/class-zoninator-api-filter-search.php | 18 +++++++++--------- .../class-zoninator-api-schema-converter.php | 8 ++++---- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/includes/class-zoninator-api-controller.php b/includes/class-zoninator-api-controller.php index 79519db..d52e41d 100644 --- a/includes/class-zoninator-api-controller.php +++ b/includes/class-zoninator-api-controller.php @@ -301,7 +301,7 @@ public function update_zone_permissions_check( $request ) { } public function is_numeric( $item ) { - // see https://github.com/WP-API/WP-API/issues/1520 on why we do not use is_numeric directly + // see https://github.com/WP-API/WP-API/issues/1520 on why we do not use is_numeric directly. return is_numeric( $item ); } @@ -310,12 +310,12 @@ public function is_numeric_array( $items ) { } public function strip_slashes( $item ) { - // see https://github.com/WP-API/WP-API/issues/1520 on why we do not use stripslashes directly + // see https://github.com/WP-API/WP-API/issues/1520 on why we do not use stripslashes directly. return stripslashes( $item ); } public function strip_tags( $item ) { - // see https://github.com/WP-API/WP-API/issues/1520 on why we do not use strip_tags directly + // see https://github.com/WP-API/WP-API/issues/1520 on why we do not use strip_tags directly. return strip_tags( $item ); } diff --git a/includes/class-zoninator-api-filter-search.php b/includes/class-zoninator-api-filter-search.php index f3c8ac8..3dbaeef 100644 --- a/includes/class-zoninator-api-filter-search.php +++ b/includes/class-zoninator-api-filter-search.php @@ -12,16 +12,16 @@ /** * Class WP_Job_Manager_Filters_Status */ -class Zoninator_Api_Filter_Search extends Zoninator_REST_Model_Declaration { +class Zoninator_Api_Filter_Search extends Zoninator_REST_Model { /** * Declare our fields * - * @param Zoninator_REST_Environment $env Def. * @return array * @throws Zoninator_REST_Exception Exc. */ - public function declare_fields( $env ) { + public function declare_fields() { + $env = $this->get_environment(); return array( $env->field( 'term', __( 'search term', 'zoninator' ) ) ->with_type( $env->type( 'string' ) ) @@ -51,8 +51,8 @@ public function declare_fields( $env ) { * @param mixed $item The item. * @return bool */ - public function is_numeric( $model, $item ) { - // see https://github.com/WP-API/WP-API/issues/1520 on why we do not use is_numeric directly + public function is_numeric( $item ) { + // see https://github.com/WP-API/WP-API/issues/1520 on why we do not use is_numeric directly. return is_numeric( $item ); } @@ -62,17 +62,17 @@ public function is_numeric( $model, $item ) { * @param mixed $item Item. * @return string */ - public function strip_slashes( $model, $item ) { - // see https://github.com/WP-API/WP-API/issues/1520 on why we do not use stripslashes directly + public function strip_slashes( $item ) { + // see https://github.com/WP-API/WP-API/issues/1520 on why we do not use stripslashes directly. return stripslashes( $item ); } - public function strip_tags( $model, $item ) { + public function strip_tags( $item ) { return strip_tags( $item ); } function date_before_set( $model, $item ) { - return $this->strip_tags( $model, $this->strip_slashes( $model, $item ) ); + return $this->strip_tags( $this->strip_slashes( $item ) ); } } diff --git a/includes/class-zoninator-api-schema-converter.php b/includes/class-zoninator-api-schema-converter.php index 5b1b3c2..85d39fe 100644 --- a/includes/class-zoninator-api-schema-converter.php +++ b/includes/class-zoninator-api-schema-converter.php @@ -4,11 +4,11 @@ class Zoninator_Api_Schema_Converter { /** * As Schema * - * @param Zoninator_REST_Model_Definition $model_definition Def. + * @param Zoninator_REST_Model $model_definition Def. * @return mixed */ public function as_schema( $model_definition ) { - $fields = $model_definition->get_field_declarations(); + $fields = $model_definition->get_fields(); $properties = array(); $required = array(); foreach ( $fields as $field_declaration ) { @@ -39,11 +39,11 @@ public function as_schema( $model_definition ) { /** * As Schema * - * @param Zoninator_REST_Model_Definition $model_definition Def. + * @param Zoninator_REST_Model $model_definition Def. * @return array */ public function as_args( $model_definition ) { - $fields = $model_definition->get_field_declarations(); + $fields = $model_definition->get_fields(); $result = array(); foreach ( $fields as $field_declaration ) { $type_schema = $field_declaration->get_type()->schema(); From 33c934f65325e2925137427ecb0bb1ff2788fdc3 Mon Sep 17 00:00:00 2001 From: Panos Kountanis Date: Fri, 8 Sep 2017 18:43:29 +0300 Subject: [PATCH 25/31] remove acceptance tests for now (we can always restore them latef from history) --- tests/acceptance/test_zoninator_admin.js | 276 ----------------------- tests/run_acceptance_tests.sh | 3 - 2 files changed, 279 deletions(-) delete mode 100644 tests/acceptance/test_zoninator_admin.js delete mode 100755 tests/run_acceptance_tests.sh diff --git a/tests/acceptance/test_zoninator_admin.js b/tests/acceptance/test_zoninator_admin.js deleted file mode 100644 index 3ac1699..0000000 --- a/tests/acceptance/test_zoninator_admin.js +++ /dev/null @@ -1,276 +0,0 @@ -/* - -### Zoninator Admin Acceptance tests - -Requires Linux/OSX and working CasperJS and WordPress installations. - -Links - -- CasperJS: http://casperjs.org/ -- Local WordPress setup: https://github.com/Chassis/Chassis - or https://roots.io/trellis/ or https://github.com/Varying-Vagrant-Vagrants/VVV - -### Usage - -expects that the following environment variables are set: - - export WP_INT_BASEURL='http://example.com' - export WP_INT_ADMIN_ROOT='/wp/wp-admin' - export WP_INT_ADMIN_USERNAME='user' - export WP_INT_ADMIN_PASS='pass' - export WP_INT_DEBUG='' - -### Running the tests - - casperjs test tests/acceptance/test_zoninator_admin.js - -for additional debug data run `WP_INT_DEBUG=1 casperjs test tests/acceptance/test_zoninator_admin.js` -*/ - -var system = require('system'); - -var WAIT_TIMEOUT = 5000; - -var baseUrl = system.env.WP_INT_BASEURL || 'http://vagrant.local'; -var adminRoot = system.env.WP_INT_ADMIN_ROOT || '/wp/wp-admin'; -var adminUserName = system.env.WP_INT_ADMIN_USERNAME || 'admin'; -var adminPassword = system.env.WP_INT_ADMIN_PASS || 'password'; -var debug = (system.env.WP_INT_DEBUG) ? true : false; - -var adminUrl = baseUrl + adminRoot, - adminAddNewPostUrl = adminUrl + '/post-new.php', - adminPostIndex = adminUrl + '/edit.php', - adminZoninatorPage = adminUrl + '/admin.php?page=zoninator', - adminActivePluginIndex = adminUrl + '/plugins.php?plugin_status=active', - integrationTestZoneTitle = 'Integration Test Zone'; - - -if (debug) { - casper.options.verbose = true; - casper.options.logLevel = 'debug'; -} - - -casper.test.begin('Zoninator Acceptance Tests', { - test: function (test) { - "use strict"; - - casper.zoneDeleted = function (casper) { - var zones = this.fetchText('.zone-tab'); - return (zones.indexOf(integrationTestZoneTitle) === -1); - }; - - casper.assertPostCanBeRemovedFromZone = function (postId) { - casper.then(function () { - this.echo('When I press Remove on zone post with id ' + postId); - var selector = '#zone-post-' + postId; - casper.waitForSelector(selector, function () { - casper.thenClick(selector + ' .delete', function () { - }); - - casper.then(function () { - this.echo('And I wait for the transition effect'); - this.wait(1000); - }); - - casper.then(function () { - test.assertNotExists(selector, 'Then the post is removed from the Zone'); - }); - }); - }); - }; - - casper.assertCanAddPostToZone = function (postId) { - casper.waitForSelector('#zone-post-latest', function () { - this.echo('When I select the post with id ' + postId); - this.fillSelectors('.zone-search-wrapper', { - '#zone-post-latest' : postId - }, false); - }); - - casper.then(function () { - casper.waitForSelector('#zone-post-' + postId, function () { - test.assertExists('#zone-post-' + postId, 'Then Post with id ' + postId + ' is added to the Zone'); - }); - }); - }; - - casper.setFilter('page.confirm', function(message) { - this.echo('And the Page displayed a message that will be confirmed: ' + message); - return true; - }); - - casper.on('resource.requested', function (request) { - // Wait on All REST API AJAX requests - // This should be called within the test context and - // we should not be using PhatomJS onResourceRequested - // as it causes some weirdness - if (request.url.indexOf('zoninator/v1') >= 0) { - var formattedRequest = [request.method, request.url].join(' '); - this.echo('And I Wait until XHR Request completes: ' + formattedRequest, 'info'); - casper.waitForResource(request.url, function(){ - this.echo('And the XHR request is completed: ' + formattedRequest, 'info'); - this.wait(250); - }, function(){ - this.echo('But the XHR request times out: ' + formattedRequest, 'info'); - }, WAIT_TIMEOUT); - } - }); - - casper.on('resource.received', function (response){ - // Capture responses for all REST API AJAX requests - if (response.url.indexOf('zoninator/v1') >= 0) { - var parts = ['id:', response.id, 'status:', response.status, 'url:', response.url] - this.log('XHR Response received: ' + parts.join(' '), 'info'); - } - }); - - casper.start(adminUrl, function() { - this.echo('When I visit ' + adminUrl); - test.assertTitle("WordPress Site › Log In", "I am at the login page"); - test.assertExists('form[name="loginform"]', "And I can find a login form"); - this.echo('When I fill the login form and submit'); - this.fill('form[name="loginform"]', { - log: adminUserName, - pwd: adminPassword - }, true); - }); - - casper.then(function() { - test.assertTitle('Dashboard ‹ WordPress Site — WordPress', - 'I am at WordPress Admin Dashboard'); - }); - - casper.thenOpen(adminPostIndex); - - casper.waitForSelector('tr.status-publish', function () { - test.assertExists('tr.status-publish', - 'The site has published posts'); - }); - - casper.thenOpen(adminActivePluginIndex); - - casper.waitForSelector('.plugin-title', function () { - test.assertSelectorHasText('.plugin-title', - 'Zone Manager (Zoninator)', - 'Zoninator Plugin is active'); - }); - - casper.thenOpen(adminZoninatorPage); - - casper.then(function () { - test.assertTitle('Zoninator ‹ WordPress Site — WordPress', - 'Zoninator Page Exists'); - }); - - casper.then(function () { - if (!this.zoneDeleted()) { - this.echo('Test Zone Found... cleaning up'); - casper.then(function () { - this.clickLabel(integrationTestZoneTitle, 'a'); - }); - - casper.then(function () { - this.clickLabel('Delete','a'); - }); - } - }); - - casper.waitForSelector('form[id="zone-info"]', function () { - test.assertExists('form[id="zone-info"]', "zone-info form is found"); - this.fill('form[id="zone-info"]', { - name: integrationTestZoneTitle, - description: 'Zone used by integration tests and can be safely deleted' - }, true); - }); - - casper.waitForSelector('.zone-tab', function () { - test.assertSelectorHasText('.zone-tab', - integrationTestZoneTitle, - 'Integration Test Zone created'); - }); - - casper.then(function () { - this.clickLabel(integrationTestZoneTitle, 'span', - 'I Click on the zone Titled ' + integrationTestZoneTitle); - }); - - casper.then(function () { - test.assertUrlMatch(/zoninator&action=edit&zone_id=/, 'Then Edit Zone Page is Rendered'); - test.assertSelectorHasText('#zone-description', - 'Zone used by integration tests and can be safely deleted', - 'And The Zone contains the expected description'); - }); - - casper.waitForSelector('form[id="zone-info"]', function () { - test.assertExists('form[id="zone-info"]', - 'zone-info form is found and ready for Editing'); - this.fill('form[id="zone-info"]', { - description: 'Edited Zone used by integration tests and can be safely deleted' - }, true); - }); - - casper.waitForSelector('#zone-description', function () { - test.assertUrlMatch(/zoninator&action=edit&zone_id=/, 'Then Edit Zone Page is Rendered'); - test.assertSelectorHasText('#zone-description', - 'Edited Zone used by integration tests and can be safely deleted', - 'And the Edited Zone contains expected description'); - - }); - - var recentPosts = []; - - casper.waitForSelector('#zone-post-latest', function () { - recentPosts = this.getElementsAttribute('#zone-post-latest > option', 'value'); - this.echo('And the following recent posts are available: [' + recentPosts + ']'); - test.assertTrue(recentPosts.length > 2, - 'And I have at least two posts that can be added to the Zone'); - }); - - casper.then(function () { - this.assertCanAddPostToZone(recentPosts[1]); - }); - - casper.then(function () { - this.assertCanAddPostToZone(recentPosts[2]); - }); - - casper.waitForSelector('#zone-post-latest', function () { - var recentPostsAfterAdditions = this.getElementsAttribute('#zone-post-latest > option', 'value'); - var selectBoxDifference = recentPosts.length - recentPostsAfterAdditions.length; - test.assertTrue(selectBoxDifference === 2, 'Then the selectable recent posts decrease by 2'); - }); - - casper.then(function () { - this.assertPostCanBeRemovedFromZone(recentPosts[2]); - }); - - casper.then(function () { - this.assertPostCanBeRemovedFromZone(recentPosts[1]); - }); - - casper.waitForSelector('#zone-post-latest', function () { - var recentPostsAfterAdditions = this.getElementsAttribute('#zone-post-latest > option', 'value'); - var selectBoxDifference = recentPosts.length - recentPostsAfterAdditions.length; - test.assertTrue(selectBoxDifference === 2, 'Then the selectable recent posts do not increase by 2 (and this is unexpected)'); - }); - - // Deleting a zone - - casper.then(function () { - this.clickLabel(integrationTestZoneTitle, 'span'); - }); - - casper.then(function () { - this.clickLabel('Delete','a'); - }); - - casper.then(function () { - test.assertTrue(this.zoneDeleted(), 'A Zone can be deleted'); - }); - - casper.run(function() { - test.done(); - }); - } -}); diff --git a/tests/run_acceptance_tests.sh b/tests/run_acceptance_tests.sh deleted file mode 100755 index 80469ea..0000000 --- a/tests/run_acceptance_tests.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -casperjs test tests/acceptance/test_zoninator_admin.js From 3fc98bad5700e12a0fba6b4ebe47a2f0bcb12e58 Mon Sep 17 00:00:00 2001 From: Kuba Birecki Date: Fri, 10 Nov 2017 18:58:08 +0100 Subject: [PATCH 26/31] Prevent characters from being removed from name & description during validation --- includes/class-zoninator-api-controller.php | 24 ++++++++------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/includes/class-zoninator-api-controller.php b/includes/class-zoninator-api-controller.php index d52e41d..ff8bbfa 100644 --- a/includes/class-zoninator-api-controller.php +++ b/includes/class-zoninator-api-controller.php @@ -121,7 +121,7 @@ function get_zones( $request ) { function create_zone( $request ) { $name = $this->_get_param( $request, 'name', '' ); $slug = $this->_get_param( $request, 'slug', $name ); - $description = $this->_get_param( $request, 'description', '', 'strip_tags' ); + $description = $this->_get_param( $request, 'description', '' ); $result = $this->instance->insert_zone( $slug, $name, array( 'description' => $description, @@ -309,14 +309,8 @@ public function is_numeric_array( $items ) { return count( $items ) === count( array_filter( $items, 'is_numeric') ); } - public function strip_slashes( $item ) { - // see https://github.com/WP-API/WP-API/issues/1520 on why we do not use stripslashes directly. - return stripslashes( $item ); - } - - public function strip_tags( $item ) { - // see https://github.com/WP-API/WP-API/issues/1520 on why we do not use strip_tags directly. - return strip_tags( $item ); + public function sanitize_string( $item ) { + return htmlentities( stripslashes( $item ) ); } /** @@ -342,19 +336,19 @@ public function _params_for_create_zone() { return array( 'name' => array( 'type' => 'string', - 'sanitize_callback' => array( $this, 'strip_slashes' ), + 'sanitize_callback' => array( $this, 'sanitize_string' ), 'default' => '', 'required' => false ), 'slug' => array( 'type' => 'string', - 'sanitize_callback' => array( $this, 'strip_slashes' ), + 'sanitize_callback' => array( $this, 'sanitize_string' ), 'default' => '', 'required' => false, ), 'description' => array( 'type' => 'string', - 'sanitize_callback' => array( $this, 'strip_tags' ), + 'sanitize_callback' => array( $this, 'sanitize_string' ), 'default' => '', 'required' => false, ) @@ -365,17 +359,17 @@ public function _params_for_update_zone() { return array( 'name' => array( 'type' => 'string', - 'sanitize_callback' => array( $this, 'strip_slashes' ), + 'sanitize_callback' => array( $this, 'sanitize_string' ), 'required' => false ), 'slug' => array( 'type' => 'string', - 'sanitize_callback' => array( $this, 'strip_slashes' ), + 'sanitize_callback' => array( $this, 'sanitize_string' ), 'required' => false, ), 'description' => array( 'type' => 'string', - 'sanitize_callback' => array( $this, 'strip_tags' ), + 'sanitize_callback' => array( $this, 'sanitize_string' ), 'required' => false, ) ); From 5e32cebfe85a886a271551ebb5172497226e0e2b Mon Sep 17 00:00:00 2001 From: Kuba Birecki Date: Fri, 15 Dec 2017 11:33:09 +0100 Subject: [PATCH 27/31] Add a test for zone details containing special characters --- .../class-zoninator-api-controller-test.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/unit/class-zoninator-api-controller-test.php b/tests/unit/class-zoninator-api-controller-test.php index 28eb799..16e392c 100644 --- a/tests/unit/class-zoninator-api-controller-test.php +++ b/tests/unit/class-zoninator-api-controller-test.php @@ -216,6 +216,24 @@ function test_create_zone_fail_if_invalid_data() { $this->assertResponseStatus( $response, 400 ); } + /** + * T test_create_zone_with_special_chars + * + * @throws Exception E + */ + function test_create_zone_with_special_chars() { + $this->login_as_admin(); + $response = $this->post( '/zoninator/v1/zones', array( + 'name' => '&<>!@#(', + 'slug' => 'test-zone', + 'description' => '&<>!@#(' + ) ); + $data = $response->get_data(); + $this->assertResponseStatus( $response, 201 ); + $this->assertEquals( $data['name'], '&<>!@#(' ); + $this->assertEquals( $data['description'], '&<>!@#(' ); + } + /** * T test_update_zone_responds_with_success_when_method_put * From 2d337d41e5b41f926942a13c0d9dfe1930783e99 Mon Sep 17 00:00:00 2001 From: Kuba Birecki Date: Fri, 15 Dec 2017 11:57:02 +0100 Subject: [PATCH 28/31] Ensure WP-Admin interface also properly escapes htmlentities --- zoninator.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/zoninator.php b/zoninator.php index e831fb8..45154bd 100644 --- a/zoninator.php +++ b/zoninator.php @@ -214,10 +214,10 @@ function admin_controller() { $this->verify_nonce( $action ); $this->verify_access( $action, $zone_id ); - $name = $this->_get_post_var( 'name' ); + $name = $this->_get_post_var( 'name', '', array( $this, '_sanitize_value' ) ); $slug = $this->_get_post_var( 'slug', sanitize_title( $name ) ); $details = array( - 'description' => $this->_get_post_var( 'description', '', 'strip_tags' ) + 'description' => $this->_get_post_var( 'description', '', array( $this, '_sanitize_value' ) ) ); // TODO: handle additional properties @@ -1563,6 +1563,10 @@ function _validate_category_filter( $cat ) { return $cat && get_term_by( 'id', $cat, 'category' ); } + function _sanitize_value( $var ) { + return htmlentities( stripslashes( $var ) ); + } + function _get_value_or_default( $var, $object, $default = '', $sanitize_callback = '' ) { if( is_object( $object ) ) $value = ! empty( $object->$var ) ? $object->$var : $default; From 518a8f199a839222365eb4fc4a9b74bd7933e854 Mon Sep 17 00:00:00 2001 From: Kuba Birecki Date: Tue, 26 Dec 2017 21:46:47 +0100 Subject: [PATCH 29/31] Restore lock-zone REST endpoint --- includes/class-zoninator-api-controller.php | 39 +++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/includes/class-zoninator-api-controller.php b/includes/class-zoninator-api-controller.php index ff8bbfa..b90e7da 100644 --- a/includes/class-zoninator-api-controller.php +++ b/includes/class-zoninator-api-controller.php @@ -90,6 +90,13 @@ function setup() { ->permissions( 'update_zone_permissions_check' ) ->args( '_get_zone_post_rest_route_params' ) ); + + $this->add_route( 'zones/(?P[\d]+)/lock' ) + ->add_action( + $this->action( 'update', 'zone_update_lock' ) + ->permissions( 'update_zone_permissions_check' ) + ->args( '_get_zone_id_param' ) + ); } /** @@ -259,6 +266,38 @@ function update_zone_posts( $request ) { return $this->ok( array( 'success' => true ) ); } + /** + * Update the zone's lock + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|WP_REST_Response + */ + function zone_update_lock( $request ) { + $zone_id = $this->_get_param( $request, 'zone_id', 0, 'absint' ); + if ( ! $zone_id ) { + return $this->_bad_request(self::ZONE_ID_REQUIRED, __('zone id required', 'zoninator')); + } + + $zone = $this->instance->get_zone( $zone_id ); + if ( ! $zone ) { + return $this->not_found( $this->translations[ self::INVALID_ZONE_ID ] ); + } + + $zone_locked = $this->instance->is_zone_locked( $zone ); + if ( $zone_locked ) { + $locking_user = get_userdata( $zone_locked ); + return new WP_REST_Response( array( + 'zone_id' => $this->instance->get_zone_id( $zone ), + 'locking_user' => $locking_user->display_name, + ), 400); + } + + $this->instance->lock_zone( $zone_id ); + return new WP_REST_Response( array( + 'zone_id' => $this->instance->get_zone_id( $zone ), + ), 200); + } + /** * Check if a given request has access to the zones index. * From de75603a85b2a620257d161f661ecf58090236ca Mon Sep 17 00:00:00 2001 From: Kuba Birecki Date: Mon, 1 Jan 2018 16:42:23 +0100 Subject: [PATCH 30/31] Add timeout and max_lock_period properties to the lock endponint's response --- includes/class-zoninator-api-controller.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/includes/class-zoninator-api-controller.php b/includes/class-zoninator-api-controller.php index b90e7da..c751de0 100644 --- a/includes/class-zoninator-api-controller.php +++ b/includes/class-zoninator-api-controller.php @@ -288,14 +288,16 @@ function zone_update_lock( $request ) { $locking_user = get_userdata( $zone_locked ); return new WP_REST_Response( array( 'zone_id' => $this->instance->get_zone_id( $zone ), - 'locking_user' => $locking_user->display_name, + 'blocked' => true, ), 400); } $this->instance->lock_zone( $zone_id ); return new WP_REST_Response( array( 'zone_id' => $this->instance->get_zone_id( $zone ), - ), 200); + 'timeout' => $this->instance->zone_lock_period, + 'max_lock_period' => $this->instance->zone_max_lock_period, + ), 200 ); } /** From 4dfda5d2af66656f1f3379a5a2e9c576b57ae778 Mon Sep 17 00:00:00 2001 From: Kuba Birecki Date: Wed, 31 Jan 2018 12:18:38 +0100 Subject: [PATCH 31/31] Build Mixtape --- .gitignore | 1 - .../class-zoninator-rest-bootstrap.php | 193 +++++ .../class-zoninator-rest-classloader.php | 145 ++++ .../class-zoninator-rest-controller.php | 285 ++++++++ .../class-zoninator-rest-environment.php | 409 +++++++++++ .../class-zoninator-rest-exception.php | 17 + .../class-zoninator-rest-expect.php | 65 ++ .../class-zoninator-rest-model.php | 686 ++++++++++++++++++ .../class-zoninator-rest-type.php | 90 +++ ...ninator-rest-controller-bundle-builder.php | 98 +++ ...class-zoninator-rest-controller-action.php | 166 +++++ ...class-zoninator-rest-controller-bundle.php | 99 +++ .../class-zoninator-rest-controller-crud.php | 199 +++++ ...ss-zoninator-rest-controller-extension.php | 83 +++ .../class-zoninator-rest-controller-model.php | 165 +++++ .../class-zoninator-rest-controller-route.php | 90 +++ ...ass-zoninator-rest-controller-settings.php | 86 +++ .../data/class-zoninator-rest-data-mapper.php | 94 +++ .../class-zoninator-rest-data-serializer.php | 59 ++ ...ass-zoninator-rest-data-store-abstract.php | 72 ++ ...lass-zoninator-rest-data-store-builder.php | 85 +++ ...ninator-rest-data-store-customposttype.php | 178 +++++ .../class-zoninator-rest-data-store-nil.php | 66 ++ ...class-zoninator-rest-data-store-option.php | 110 +++ ...class-zoninator-rest-field-declaration.php | 434 +++++++++++ ...ninator-rest-field-declaration-builder.php | 290 ++++++++ ...lass-zoninator-rest-interfaces-builder.php | 22 + ...-zoninator-rest-interfaces-classloader.php | 25 + ...s-zoninator-rest-interfaces-controller.php | 26 + .../class-zoninator-rest-interfaces-model.php | 151 ++++ ...-zoninator-rest-interfaces-registrable.php | 24 + .../class-zoninator-rest-interfaces-type.php | 50 ++ ...ator-rest-interfaces-controller-bundle.php | 25 + ...s-zoninator-rest-interfaces-data-store.php | 52 ++ ...nator-rest-interfaces-model-collection.php | 22 + ...ator-rest-interfaces-model-declaration.php | 79 ++ ...r-rest-interfaces-permissions-provider.php | 20 + .../class-zoninator-rest-model-collection.php | 42 ++ ...class-zoninator-rest-model-declaration.php | 116 +++ .../class-zoninator-rest-model-definition.php | 339 +++++++++ .../class-zoninator-rest-model-settings.php | 163 +++++ ...ss-zoninator-rest-model-validationdata.php | 75 ++ ...inator-rest-model-declaration-settings.php | 174 +++++ ...oninator-rest-model-definition-builder.php | 108 +++ .../class-zoninator-rest-permissions-any.php | 27 + .../type/class-zoninator-rest-type-array.php | 42 ++ .../class-zoninator-rest-type-boolean.php | 45 ++ .../class-zoninator-rest-type-integer.php | 65 ++ .../class-zoninator-rest-type-nullable.php | 75 ++ .../type/class-zoninator-rest-type-number.php | 55 ++ .../class-zoninator-rest-type-registry.php | 125 ++++ .../type/class-zoninator-rest-type-string.php | 58 ++ .../class-zoninator-rest-type-typedarray.php | 71 ++ 53 files changed, 6340 insertions(+), 1 deletion(-) create mode 100644 lib/zoninator_rest/class-zoninator-rest-bootstrap.php create mode 100644 lib/zoninator_rest/class-zoninator-rest-classloader.php create mode 100644 lib/zoninator_rest/class-zoninator-rest-controller.php create mode 100644 lib/zoninator_rest/class-zoninator-rest-environment.php create mode 100644 lib/zoninator_rest/class-zoninator-rest-exception.php create mode 100644 lib/zoninator_rest/class-zoninator-rest-expect.php create mode 100644 lib/zoninator_rest/class-zoninator-rest-model.php create mode 100644 lib/zoninator_rest/class-zoninator-rest-type.php create mode 100644 lib/zoninator_rest/controller/bundle/class-zoninator-rest-controller-bundle-builder.php create mode 100644 lib/zoninator_rest/controller/class-zoninator-rest-controller-action.php create mode 100644 lib/zoninator_rest/controller/class-zoninator-rest-controller-bundle.php create mode 100644 lib/zoninator_rest/controller/class-zoninator-rest-controller-crud.php create mode 100644 lib/zoninator_rest/controller/class-zoninator-rest-controller-extension.php create mode 100644 lib/zoninator_rest/controller/class-zoninator-rest-controller-model.php create mode 100644 lib/zoninator_rest/controller/class-zoninator-rest-controller-route.php create mode 100644 lib/zoninator_rest/controller/class-zoninator-rest-controller-settings.php create mode 100644 lib/zoninator_rest/data/class-zoninator-rest-data-mapper.php create mode 100644 lib/zoninator_rest/data/class-zoninator-rest-data-serializer.php create mode 100644 lib/zoninator_rest/data/store/class-zoninator-rest-data-store-abstract.php create mode 100644 lib/zoninator_rest/data/store/class-zoninator-rest-data-store-builder.php create mode 100644 lib/zoninator_rest/data/store/class-zoninator-rest-data-store-customposttype.php create mode 100644 lib/zoninator_rest/data/store/class-zoninator-rest-data-store-nil.php create mode 100644 lib/zoninator_rest/data/store/class-zoninator-rest-data-store-option.php create mode 100644 lib/zoninator_rest/field/class-zoninator-rest-field-declaration.php create mode 100644 lib/zoninator_rest/field/declaration/class-zoninator-rest-field-declaration-builder.php create mode 100644 lib/zoninator_rest/interfaces/class-zoninator-rest-interfaces-builder.php create mode 100644 lib/zoninator_rest/interfaces/class-zoninator-rest-interfaces-classloader.php create mode 100644 lib/zoninator_rest/interfaces/class-zoninator-rest-interfaces-controller.php create mode 100644 lib/zoninator_rest/interfaces/class-zoninator-rest-interfaces-model.php create mode 100644 lib/zoninator_rest/interfaces/class-zoninator-rest-interfaces-registrable.php create mode 100644 lib/zoninator_rest/interfaces/class-zoninator-rest-interfaces-type.php create mode 100644 lib/zoninator_rest/interfaces/controller/class-zoninator-rest-interfaces-controller-bundle.php create mode 100644 lib/zoninator_rest/interfaces/data/class-zoninator-rest-interfaces-data-store.php create mode 100644 lib/zoninator_rest/interfaces/model/class-zoninator-rest-interfaces-model-collection.php create mode 100644 lib/zoninator_rest/interfaces/model/class-zoninator-rest-interfaces-model-declaration.php create mode 100644 lib/zoninator_rest/interfaces/permissions/class-zoninator-rest-interfaces-permissions-provider.php create mode 100644 lib/zoninator_rest/model/class-zoninator-rest-model-collection.php create mode 100644 lib/zoninator_rest/model/class-zoninator-rest-model-declaration.php create mode 100644 lib/zoninator_rest/model/class-zoninator-rest-model-definition.php create mode 100644 lib/zoninator_rest/model/class-zoninator-rest-model-settings.php create mode 100644 lib/zoninator_rest/model/class-zoninator-rest-model-validationdata.php create mode 100644 lib/zoninator_rest/model/declaration/class-zoninator-rest-model-declaration-settings.php create mode 100644 lib/zoninator_rest/model/definition/class-zoninator-rest-model-definition-builder.php create mode 100644 lib/zoninator_rest/permissions/class-zoninator-rest-permissions-any.php create mode 100644 lib/zoninator_rest/type/class-zoninator-rest-type-array.php create mode 100644 lib/zoninator_rest/type/class-zoninator-rest-type-boolean.php create mode 100644 lib/zoninator_rest/type/class-zoninator-rest-type-integer.php create mode 100644 lib/zoninator_rest/type/class-zoninator-rest-type-nullable.php create mode 100644 lib/zoninator_rest/type/class-zoninator-rest-type-number.php create mode 100644 lib/zoninator_rest/type/class-zoninator-rest-type-registry.php create mode 100644 lib/zoninator_rest/type/class-zoninator-rest-type-string.php create mode 100644 lib/zoninator_rest/type/class-zoninator-rest-type-typedarray.php diff --git a/.gitignore b/.gitignore index deea66e..ffe5237 100644 --- a/.gitignore +++ b/.gitignore @@ -205,5 +205,4 @@ dkms.conf # tmp -lib vendor diff --git a/lib/zoninator_rest/class-zoninator-rest-bootstrap.php b/lib/zoninator_rest/class-zoninator-rest-bootstrap.php new file mode 100644 index 0000000..0af4812 --- /dev/null +++ b/lib/zoninator_rest/class-zoninator-rest-bootstrap.php @@ -0,0 +1,193 @@ +class_loader = $class_loader; + } + + /** + * Check compatibility of PHP Version. + * + * @return bool + */ + public static function is_compatible() { + return version_compare( phpversion(), self::MINIMUM_PHP_VERSION, '>=' ); + } + + /** + * Get Base Dir + * + * @return string + */ + public static function get_base_dir() { + return untrailingslashit( dirname( __FILE__ ) ); + } + + /** + * Create a Bootstrap, unless we are using a really early php version (< 5.3.0) + * + * @param Zoninator_REST_Interfaces_Classloader|null $class_loader The class loader to use. + * @return Zoninator_REST_Bootstrap|null + */ + public static function create( $class_loader = null ) { + if ( empty( $class_loader ) ) { + include_once( 'interfaces/class-zoninator-rest-interfaces-classloader.php' ); + include_once( 'class-zoninator-rest-classloader.php' ); + $prefix = str_replace( '_Bootstrap', '', __CLASS__ ); + $base_dir = self::get_base_dir(); + $class_loader = new Zoninator_REST_Classloader( $prefix, $base_dir ); + } + return new self( $class_loader ); + } + + /** + * Run the app + * + * @return bool + */ + public function run() { + if ( ! self::is_compatible() ) { + return false; + } + $this->load() + ->environment()->start(); + return true; + } + + /** + * Optional: Instead of calling load() you can + * register as an auto-loader + * + * @return Zoninator_REST_Bootstrap $this + */ + function register_autoload() { + if ( function_exists( 'spl_autoload_register' ) ) { + spl_autoload_register( array( $this->class_loader(), 'load_class' ), true ); + } + return $this; + } + + /** + * Loads all classes + * + * @return Zoninator_REST_Bootstrap $this + * @throws Exception In case a class/file is not found. + */ + function load() { + $this->class_loader() + ->load_class( 'Interfaces_Data_Store' ) + ->load_class( 'Interfaces_Registrable' ) + ->load_class( 'Interfaces_Type' ) + ->load_class( 'Interfaces_Model' ) + ->load_class( 'Interfaces_Builder' ) + ->load_class( 'Interfaces_Model_Collection' ) + ->load_class( 'Interfaces_Controller' ) + ->load_class( 'Interfaces_Controller_Bundle' ) + ->load_class( 'Interfaces_Permissions_Provider' ) + ->load_class( 'Exception' ) + ->load_class( 'Expect' ) + ->load_class( 'Environment' ) + ->load_class( 'Type' ) + ->load_class( 'Type_String' ) + ->load_class( 'Type_Integer' ) + ->load_class( 'Type_Number' ) + ->load_class( 'Type_Boolean' ) + ->load_class( 'Type_Array' ) + ->load_class( 'Type_TypedArray' ) + ->load_class( 'Type_Nullable' ) + ->load_class( 'Type_Registry' ) + ->load_class( 'Data_Store_Nil' ) + ->load_class( 'Data_Store_Abstract' ) + ->load_class( 'Data_Store_CustomPostType' ) + ->load_class( 'Data_Store_Option' ) + ->load_class( 'Permissions_Any' ) + ->load_class( 'Field_Declaration' ) + ->load_class( 'Field_Declaration_Builder' ) + ->load_class( 'Model' ) + ->load_class( 'Model_Settings' ) + ->load_class( 'Model_Collection' ) + ->load_class( 'Controller' ) + ->load_class( 'Controller_Action' ) + ->load_class( 'Controller_Model' ) + ->load_class( 'Controller_Settings' ) + ->load_class( 'Controller_Route' ) + ->load_class( 'Controller_CRUD' ) + ->load_class( 'Controller_Bundle' ) + ->load_class( 'Controller_Extension' ) + ->load_class( 'Controller_Bundle_Builder' ); + + return $this; + } + + /** + * Load Unit Testing Base Classes + * + * @return Zoninator_REST_Bootstrap $this + */ + function load_testing_classes() { + $this->class_loader() + ->load_class( 'Testing_TestCase' ) + ->load_class( 'Testing_Model_TestCase' ) + ->load_class( 'Testing_Controller_TestCase' ); + return $this; + } + + /** + * Get the class loader + * + * @return Zoninator_REST_Classloader + */ + function class_loader() { + return $this->class_loader; + } + + /** + * Lazy-load the environment + * + * @return Zoninator_REST_Environment + */ + public function environment() { + if ( null === $this->environment ) { + $this->environment = new Zoninator_REST_Environment( $this ); + } + return $this->environment; + } +} diff --git a/lib/zoninator_rest/class-zoninator-rest-classloader.php b/lib/zoninator_rest/class-zoninator-rest-classloader.php new file mode 100644 index 0000000..2816525 --- /dev/null +++ b/lib/zoninator_rest/class-zoninator-rest-classloader.php @@ -0,0 +1,145 @@ +loaded_classes = array(); + $this->prefix = $prefix; + $this->base_dir = $base_dir; + if ( ! is_dir( $this->base_dir ) ) { + throw new Exception( 'base_dir does not exist: ' . $this->base_dir ); + } + } + + /** + * Loads a class + * + * @param string $class_name The class to load. + * + * @return Zoninator_REST_Interfaces_Classloader + * @throws Exception Throws in case include_class_file fails. + */ + public function load_class( $class_name ) { + $path = $this->get_path_to_class_file( $class_name ); + return $this->include_class_file( $path ); + } + + /** + * Get path_to_class_file + * + * @param string $class_name The class. + * + * @return string The full path to the file. + */ + public function get_path_to_class_file( $class_name ) { + return path_join( $this->base_dir, $this->class_name_to_relative_path( $class_name ) ); + } + + /** + * Class_name_to_relative_path + * + * @param string $class_name The class name. + * @param null|string $prefix The prefix. + * + * @return string + */ + public function class_name_to_relative_path( $class_name, $prefix = null ) { + $lowercase = strtolower( $this->prefixed_class_name( $class_name, $prefix ) ); + $file_name = 'class-' . str_replace( '_', '-', $lowercase ) . '.php'; + $parts = explode( '_', strtolower( $this->strip_prefix( $class_name, $prefix ) ) ); + array_pop( $parts ); + $parts[] = $file_name; + return implode( DIRECTORY_SEPARATOR, $parts ); + } + + /** + * Prefixed_class_name + * + * @param string $class_name The class name. + * @param null|string $prefix The prefix. + * + * @return string + */ + public function prefixed_class_name( $class_name, $prefix = null ) { + if ( empty( $prefix ) ) { + $prefix = $this->prefix; + } + return $prefix . '_' . $this->strip_prefix( $class_name, $prefix ); + } + + /** + * Strip_prefix + * + * @param string $class_name The class name. + * @param null|string $prefix The prefix. + * + * @return string + */ + private function strip_prefix( $class_name, $prefix = null ) { + if ( empty( $prefix ) ) { + $prefix = $this->prefix; + } + return str_replace( $prefix, '', $class_name ); + } + + /** + * Include_class_file + * + * @param string $path_to_the_class The file path. + * + * @return string + * @throws Exception Throws when the file does not exist. + */ + private function include_class_file( $path_to_the_class ) { + if ( isset( $this->loaded_classes[ $path_to_the_class ] ) ) { + return $this; + } + if ( ! file_exists( $path_to_the_class ) ) { + throw new Exception( $path_to_the_class . ' not found' ); + } + $included = include_once( $path_to_the_class ); + $this->loaded_classes[ $path_to_the_class ] = $included; + + return $this; + } +} diff --git a/lib/zoninator_rest/class-zoninator-rest-controller.php b/lib/zoninator_rest/class-zoninator-rest-controller.php new file mode 100644 index 0000000..7181c95 --- /dev/null +++ b/lib/zoninator_rest/class-zoninator-rest-controller.php @@ -0,0 +1,285 @@ +controller_bundle = $controller_bundle; + return $this; + } + + /** + * Set the Environment for this Controller. + * + * @param Zoninator_REST_Environment|null $environment The Environment. + * @return Zoninator_REST_Controller + */ + public function set_environment( $environment ) { + $this->environment = $environment; + return $this; + } + + /** + * Register This Controller + * + * @param Zoninator_REST_Controller_Bundle $bundle The bundle to register with. + * @param Zoninator_REST_Environment $environment The Environment to use. + * @throws Zoninator_REST_Exception Throws. + * + * @return bool|WP_Error true if valid otherwise error. + */ + public function register( $bundle, $environment ) { + $this->set_controller_bundle( $bundle ); + $this->set_environment( $environment ); + $this->setup(); + Zoninator_REST_Expect::that( ! empty( $this->base ), 'Need to put a string with a backslash in $base' ); + $prefix = $this->controller_bundle->get_prefix(); + foreach ( $this->routes as $pattern => $route ) { + /** + * The route we want to register. + * + * @var Zoninator_REST_Controller_Route $route + */ + $params = $route->as_array(); + $result = register_rest_route( $prefix, $this->base . $params['pattern'], $params['actions'] ); + if ( false === $result ) { + // For now we throw on error, maybe we just need to warn though. + throw new Zoninator_REST_Exception( 'Registration failed' ); + } + } + + return true; + } + + /** + * Create Action + * + * @param string $action_name Action Name. + * @param null|string|array|callable $callback Callback. + * @return Zoninator_REST_Controller_Action + */ + public function action( $action_name, $callback = null ) { + $route_action = new Zoninator_REST_Controller_Action( $this, $action_name ); + if ( null !== $callback ) { + $route_action->callback( $callback ); + } + + return $route_action; + } + + /** + * Do any additional Configuration. Runs inside register before any register_rest_route + * + * This is a good place for overriding classes to define routes and handlers + */ + protected function setup() { + } + + /** + * Succeed + * + * @param array $data The dto. + * + * @return WP_REST_Response + */ + public function ok( $data ) { + return $this->respond( $data, self::HTTP_OK ); + } + + /** + * Created + * + * @param array $data The dto. + * + * @return WP_REST_Response + */ + public function created( $data ) { + return $this->respond( $data, self::HTTP_CREATED ); + } + + /** + * Bad request + * + * @param array|WP_Error $data The dto. + * + * @return WP_REST_Response + */ + public function bad_request( $data ) { + return $this->respond( $data, self::HTTP_BAD_REQUEST ); + } + + /** + * Not Found (404) + * + * @param string $message The message. + * + * @return WP_REST_Response + */ + public function not_found( $message ) { + return $this->respond( array( + 'message' => $message, + ), self::HTTP_NOT_FOUND ); + } + + /** + * Respond + * + * @param array|WP_REST_Response|WP_Error|mixed $data The thing. + * @param int $status The Status. + * + * @return mixed|WP_REST_Response + */ + public function respond( $data, $status ) { + if ( is_a( $data, 'WP_REST_Response' ) ) { + return $data; + } + + return new WP_REST_Response( $data, $status ); + } + + /** + * Permissions for get_items + * + * @param WP_REST_Request $request Request. + * @return bool + */ + public function index_permissions_check( $request ) { + return $this->permissions_check( $request, 'index' ); + } + + /** + * Permissions for get_item + * + * @param WP_REST_Request $request The request. + * @return bool + */ + public function show_permissions_check( $request ) { + return $this->permissions_check( $request, 'show' ); + } + + /** + * Permissions for create_item + * + * @param WP_REST_Request $request Request. + * @return bool + */ + public function create_permissions_check( $request ) { + return $this->permissions_check( $request, 'create' ); + } + + /** + * Permissions for update_item + * + * @param WP_REST_Request $request Request. + * @return bool + */ + public function update_permissions_check( $request ) { + return $this->permissions_check( $request, 'update' ); + } + + /** + * Permissions for delete + * + * @param WP_REST_Request $request Request. + * @return bool + */ + public function delete_permissions_check( $request ) { + return $this->permissions_check( $request, 'delete' ); + } + + /** + * Generic Permissions Check. + * + * @param WP_REST_Request $request Request. + * @param string $action One of (index, show, create, update, delete, any). + * @return bool + */ + function permissions_check( $request, $action = 'any' ) { + return true; + } + + /** + * Add a route + * + * @param string $pattern The route pattern (e.g. '/'). + * @return Zoninator_REST_Controller_Route + */ + function add_route( $pattern = '' ) { + $route = new Zoninator_REST_Controller_Route( $this, $pattern ); + $this->routes[ $pattern ] = $route; + return $this->routes[ $pattern ]; + } + + /** + * Get Environment + * + * @return Zoninator_REST_Environment + */ + protected function environment() { + return $this->environment; + } + + /** + * Get base url + * + * @return string + */ + function get_base() { + return rest_url( $this->controller_bundle->get_prefix() . $this->base ); + } +} diff --git a/lib/zoninator_rest/class-zoninator-rest-environment.php b/lib/zoninator_rest/class-zoninator-rest-environment.php new file mode 100644 index 0000000..77d2386 --- /dev/null +++ b/lib/zoninator_rest/class-zoninator-rest-environment.php @@ -0,0 +1,409 @@ +bootstrap = $bootstrap; + $this->has_started = false; + $this->rest_apis = array(); + $this->variables = array(); + $this->model_definitions = array(); + $this->type_registry = new Zoninator_REST_Type_Registry(); + $this->type_registry->initialize( $this ); + // initialize our array vars. + $this->array_var( self::MODELS ) + ->array_var( self::REGISTRABLE ) + ->array_var( self::BUNDLES ); + } + + /** + * Push a Builder to the Environment. + * + * All builders are evaluated lazily when needed + * + * @param string $where The queue to push the builder to. + * @param Zoninator_REST_Interfaces_Builder $builder The builder to push. + * + * @return Zoninator_REST_Environment $this + * @throws Zoninator_REST_Exception In case the $builder is not a Mixtape_Interfaces_Builder. + */ + public function push_builder( $where, $builder ) { + Zoninator_REST_Expect::that( is_string( $where ), '$where should be a string' ); + Zoninator_REST_Expect::is_a( $builder, 'Zoninator_REST_Interfaces_Builder' ); + return $this->array_var( $where, $builder ); + } + + /** + * Retrieve a previously defined Zoninator_REST_Model + * + * @param string $class the class name. + * @return Zoninator_REST_Model the definition. + * @throws Zoninator_REST_Exception Throws in case the model is not registered. + */ + public function model( $class ) { + if ( ! class_exists( $class ) ) { + throw new Zoninator_REST_Exception( $class . ' does not exist' ); + } + Zoninator_REST_Expect::that( isset( $this->model_definitions[ $class ] ), $class . ' definition does not exist' ); + return $this->model_definitions[ $class ]; + } + + /** + * Time to build pending models and bundles + * + * @param string $type One of (models, bundles). + * @return Zoninator_REST_Environment + */ + private function load_pending_builders( $type ) { + $things = $this->get( $type ); + if ( ! empty( $things ) && is_array( $things ) ) { + foreach ( $things as $pending ) { + /** + * Our pending builder. + * + * @var Zoninator_REST_Interfaces_Builder $pending Our builder. + */ + if ( self::BUNDLES === $type ) { + $this->add_rest_bundle( $pending->build() ); + } + } + } + + return $this; + } + + /** + * Start things up + * + * This should be called once our Environment is set up to our liking. + * Evaluates all Builders, creating missing REST Api and Model Definitions. + * + * + * It is recommended we hook this into 'rest_api_init'. + * + * @return Zoninator_REST_Environment $this + */ + public function start() { + if ( ! $this->bootstrap->is_compatible() ) { + // Do not even start on an incompatible system. + return $this; + } + + if ( false === $this->has_started ) { + do_action( 'mt_environment_before_start', $this, get_class( $this ) ); + $this->load_pending_builders( self::MODELS ); + $this->load_pending_builders( self::BUNDLES ); + $registrables = $this->get( self::REGISTRABLE ) ? $this->get( self::REGISTRABLE ) : array(); + foreach ( $registrables as $registrable ) { + /** + * A Registrable + * + * @var Zoninator_REST_Interfaces_Registrable $registrable + */ + $registrable->register( $this ); + } + + /** + * Use this hook to add/remove rest api bundles + * + * @param array $rest_apis The existing rest apis. + * @param Zoninator_REST_Environment $this The Environment. + */ + $rest_apis = (array) apply_filters( 'mt_environment_get_rest_apis', $this->rest_apis, $this ); + + foreach ( $rest_apis as $k => $bundle ) { + /** + * Register this bundle + * + * @var Zoninator_REST_Interfaces_Controller_Bundle + */ + $bundle->register( $this ); + } + $this->has_started = true; + do_action( 'mt_environment_after_start', $this ); + } + + return $this; + } + + /** + * Add Registrable + * + * @param Zoninator_REST_Interfaces_Registrable $registrable_thing Registrable. + * @return Zoninator_REST_Environment + * @throws Zoninator_REST_Exception When not a Zoninator_REST_Interfaces_Registrable. + */ + public function add_registrable( $registrable_thing ) { + Zoninator_REST_Expect::is_a( $registrable_thing, 'Zoninator_REST_Interfaces_Registrable' ); + $this->array_var( self::REGISTRABLE, $registrable_thing ); + return $this->define_var( get_class( $registrable_thing ), $registrable_thing ); + } + + /** + * Has Variable + * + * @param string $name Is this variable Set. + * @return bool + */ + public function has_variable( $name ) { + return isset( $this->variables[ $name ] ); + } + + /** + * Append to an array + * + * @param string $name The VarArray Name. + * @param mixed $thing The thing. + * @return Zoninator_REST_Environment + */ + public function array_var( $name, $thing = null ) { + return $this->define_var( $name, $thing, true ); + } + + /** + * Get A Variable + * + * @param string $name The Variable Name. + * @return mixed|null The variable or null + * + * @throws Zoninator_REST_Exception Name should be a string. + */ + public function get( $name ) { + Zoninator_REST_Expect::that( is_string( $name ), '$name should be a string' ); + $value = $this->has_variable( $name ) ? $this->variables[ $name ] : null; + /** + * Filter the variable value + * + * @param mixed $value The value. + * @param Zoninator_REST_Environment $this The Environemnt. + * @param string $name The var name. + * + * @return mixed + */ + return apply_filters( 'mt_variable_get', $value, $this, $name ); + } + + /** + * Def. + * + * @param string $name The Variable To Add. + * @param mixed $thing The thing that is associated with the var. + * @param bool $append If true, this var is a list. + * + * @return $this + * + * @throws Zoninator_REST_Exception When name is not a string. + */ + public function define_var( $name, $thing = null, $append = false ) { + Zoninator_REST_Expect::that( is_string( $name ), '$name should be a string' ); + if ( $append && ! $this->has_variable( $name ) ) { + $this->variables[ $name ] = array(); + } + if ( null !== $thing ) { + if ( $append ) { + $this->variables[ $name ][] = $thing; + } else { + $this->variables[ $name ] = $thing; + } + } + return $this; + } + + /** + * Auto start on rest_api_init. For more control, use ::start(); + */ + public function auto_start() { + add_action( 'rest_api_init', array( $this, 'start' ) ); + } + + /** + * Get this Environment's bootstrap instance + * + * @return Zoninator_REST_Bootstrap our bootstrap. + */ + public function get_bootstrap() { + return $this->bootstrap; + } + + /** + * Create a new Field Declaration Builder + * + * @param null|string $name Optional, the field name. + * @param null|string $description Optional, the description. + * @param null|string $field_kind The field kind (default 'field'). + * + * @return Zoninator_REST_Field_Declaration_Builder + */ + public function field( $name = null, $description = null, $field_kind = null ) { + $builder = new Zoninator_REST_Field_Declaration_Builder(); + + if ( ! empty( $name ) ) { + $builder->with_name( $name ); + } + + if ( ! empty( $description ) ) { + $builder->with_description( $description ); + } + + if ( empty( $field_kind ) ) { + $field_kind = Zoninator_REST_Field_Declaration::FIELD; + } + + $builder->with_kind( $field_kind ); + + return $builder; + } + + /** + * Get our registered types + * + * @return Zoninator_REST_Type_Registry + */ + public function get_type_registry() { + return $this->type_registry; + } + + /** + * Get a known type definition + * + * @param string $type_name The type name. + * @return Zoninator_REST_Interfaces_Type + * + * @throws Zoninator_REST_Exception When provided with an unknown/invalid type. + */ + public function type( $type_name ) { + return $this->get_type_registry()->definition( $type_name ); + } + + /** + * Define a new REST API Bundle. + * + * @param null|string|Zoninator_REST_Interfaces_Controller_Bundle $maybe_bundle_or_prefix The bundle name. + * @return Zoninator_REST_Controller_Bundle_Builder + */ + public function rest_api( $maybe_bundle_or_prefix = null ) { + if ( is_a( $maybe_bundle_or_prefix, 'Zoninator_REST_Interfaces_Controller_Bundle' ) ) { + $builder = new Zoninator_REST_Controller_Bundle_Builder( $maybe_bundle_or_prefix ); + } else { + $builder = new Zoninator_REST_Controller_Bundle_Builder(); + if ( is_string( $maybe_bundle_or_prefix ) ) { + $builder->with_prefix( $maybe_bundle_or_prefix ); + } + $builder->with_environment( $this ); + } + + $this->push_builder( self::BUNDLES, $builder ); + return $builder; + } + + /** + * Define a new Model + * + * @param string $declaration A Model class string. + * + * @return Zoninator_REST_Model + */ + function define_model( $declaration ) { + Zoninator_REST_Expect::that( class_exists( $declaration ), '$declaration string should be an existing class' ); + Zoninator_REST_Expect::that( in_array( 'Zoninator_REST_Interfaces_Model', class_implements( $declaration ), true ), '$declaration does not implement Zoninator_REST_Interfaces_Model' ); + + /** + * Create an empty Model to act as our factory (I know this is weird, see php5.2) + * + * @var Zoninator_REST_Model $factory + */ + $factory = new $declaration(); + $factory->with_environment( $this ); + $factory->with_data_store( new Zoninator_REST_Data_Store_Nil() ); + $factory->with_permissions_provider( new Zoninator_REST_Permissions_Any() ); + $this->model_definitions[ $declaration ] = $factory; + return $factory; + } + + /** + * Add a Bundle to our bundles (muse be Mixtape_Interfaces_Rest_Api_Controller_Bundle) + * + * @param Zoninator_REST_Interfaces_Controller_Bundle $bundle the bundle. + * + * @return Zoninator_REST_Environment $this + * @throws Zoninator_REST_Exception In case it's not a Zoninator_REST_Interfaces_Controller_Bundle. + */ + private function add_rest_bundle( $bundle ) { + Zoninator_REST_Expect::is_a( $bundle, 'Zoninator_REST_Interfaces_Controller_Bundle' ); + $key = $bundle->get_prefix(); + $this->rest_apis[ $key ] = $bundle; + return $this; + } +} diff --git a/lib/zoninator_rest/class-zoninator-rest-exception.php b/lib/zoninator_rest/class-zoninator-rest-exception.php new file mode 100644 index 0000000..4e990d3 --- /dev/null +++ b/lib/zoninator_rest/class-zoninator-rest-exception.php @@ -0,0 +1,17 @@ +data = array(); + + if ( isset( $args['deserialize'] ) && true === $args['deserialize'] ) { + unset( $args['deserialize'] ); + $data = $this->deserialize( $data ); + } + $this->raw_data = $data; + $data_keys = array_keys( $data ); + + foreach ( $data_keys as $key ) { + $this->set( $key, $this->raw_data[ $key ] ); + } + } + + /** + * Gets the value of a previously defined field. + * + * @param string $field_name The field name. + * @param array $args Any args. + * + * @return mixed + * @throws Zoninator_REST_Exception Fails when field is unknown. + */ + public function get( $field_name, $args = array() ) { + Zoninator_REST_Expect::that( $this->has( $field_name ), 'Field ' . $field_name . 'is not defined' ); + $fields = $this->get_fields(); + $field_declaration = $fields[ $field_name ]; + $this->set_field_if_unset( $field_declaration ); + + return $this->prepare_value( $field_declaration ); + } + + /** + * Sets a field value. + * + * @param string $field The field name. + * @param mixed $value The new field value. + * + * @return $this + * @throws Zoninator_REST_Exception Throws when trying to set an unknown field. + */ + public function set( $field, $value ) { + Zoninator_REST_Expect::that( $this->has( $field ), 'Field ' . $field . 'is not defined' ); + $fields = self::get_fields(); + /** + * The declaration. + * + * @var Zoninator_REST_Field_Declaration $field_declaration The declaration. + */ + $field_declaration = $fields[ $field ]; + if ( isset( $args['deserializing'] ) && $args['deserializing'] ) { + $value = $this->deserialize_field( $field_declaration, $value ); + } + if ( null !== $field_declaration->before_set() ) { + $val = $this->call( $field_declaration->before_set(), array( $value, $field_declaration->get_name() ) ); + } else { + $val = $field_declaration->cast_value( $value ); + } + $this->data[ $field_declaration->get_name() ] = $val; + return $this; + } + + /** + * Check if this model has a field + * + * @param string $field The field name to check. + * @return bool + */ + public function has( $field ) { + $fields = $this->get_fields(); + return isset( $fields[ $field ] ); + } + + /** + * Validate this Model's current state. + * + * @return bool|WP_Error Either true or WP_Error on failure. + */ + public function validate() { + $validation_errors = array(); + $fields = self::get_fields(); + foreach ( $fields as $key => $field_declaration ) { + $is_valid = $this->run_field_validations( $field_declaration ); + if ( is_wp_error( $is_valid ) ) { + $validation_errors[] = $is_valid->get_error_data(); + } + } + if ( count( $validation_errors ) > 0 ) { + return $this->validation_error( $validation_errors ); + } + return true; + } + + /** + * Sanitize this Model's current data. + * + * @return Zoninator_REST_Interfaces_Model $this + */ + public function sanitize() { + $fields = self::get_fields(); + foreach ( $fields as $key => $field_declaration ) { + /** + * Field Declaration. + * + * @var Zoninator_REST_Field_Declaration $field_declaration + */ + $field_name = $field_declaration->get_name(); + $value = $this->get( $field_name ); + $custom_sanitization = $field_declaration->get_sanitizer(); + if ( ! empty( $custom_sanitization ) ) { + $value = $this->call( $custom_sanitization, array( $this, $value ) ); + } else { + $value = $field_declaration->get_type()->sanitize( $value ); + } + $this->set( $field_name, $value ); + } + return $this; + } + + /** + * We got a Validation Error + * + * @param array $error_data The details. + * @return WP_Error + */ + protected function validation_error( $error_data ) { + return new WP_Error( 'validation-error', 'validation-error', $error_data ); + } + + /** + * Run Validations for this field. + * + * @param Zoninator_REST_Field_Declaration $field_declaration The field. + * + * @return bool|WP_Error + */ + protected function run_field_validations( $field_declaration ) { + if ( $field_declaration->is_kind( Zoninator_REST_Field_Declaration::DERIVED ) ) { + return true; + } + $value = $this->get( $field_declaration->get_name() ); + if ( $field_declaration->is_required() && empty( $value ) ) { + // translators: %s is usually a field name. + $message = sprintf( __( '%s cannot be empty', 'mixtape' ), $field_declaration->get_name() ); + return new WP_Error( 'required-field-empty', $message ); + } elseif ( ! $field_declaration->is_required() && ! empty( $value ) ) { + foreach ( $field_declaration->get_validations() as $validation ) { + $result = $this->call( $validation, array( $value ) ); + if ( is_wp_error( $result ) ) { + $result->add_data(array( + 'reason' => $result->get_error_messages(), + 'field' => $field_declaration->get_data_transfer_name(), + 'value' => $value, + ) ); + return $result; + } + } + } + return true; + } + + /** + * Prepare the value associated with this declaraton for output. + * + * @param Zoninator_REST_Field_Declaration $field_declaration The declaration to use. + * @return mixed + */ + private function prepare_value( $field_declaration ) { + $key = $field_declaration->get_name(); + $value = $this->data[ $key ]; + $before_return = $field_declaration->before_get(); + if ( isset( $before_return ) && ! empty( $before_return ) ) { + $value = $this->call( $before_return, array( $value, $key ) ); + } + + return $value; + } + + /** + * Sets this field's value. Used for derived fields. + * + * @param Zoninator_REST_Field_Declaration $field_declaration The field declaration. + */ + private function set_field_if_unset( $field_declaration ) { + $field_name = $field_declaration->get_name(); + if ( ! isset( $this->data[ $field_name ] ) ) { + if ( $field_declaration->is_kind( Zoninator_REST_Field_Declaration::DERIVED ) ) { + $map_from = $field_declaration->get_map_from(); + $value = $this->call( $map_from ); + $this->set( $field_name, $value ); + } else { + $this->set( $field_name, $field_declaration->get_default_value() ); + } + } + } + + /** + * Get this model class fields + * + * @param null|string $filter_by_type Filter. + * @return array + */ + public function get_fields( $filter_by_type = null ) { + $class_name = get_class( $this ); + /** + * Out model + * + * @var Zoninator_REST_Interfaces_Model $instance + */ + $instance = new $class_name(); + if ( ! isset( self::$fields_by_class_name[ $class_name ] ) ) { + $fields = $instance->declare_fields(); + self::$fields_by_class_name[ $class_name ] = self::initialize_field_map( $fields ); + } + + if ( null === $filter_by_type ) { + return self::$fields_by_class_name[ $class_name ]; + } + + $filtered = array(); + + foreach ( self::$fields_by_class_name[ $class_name ] as $field_declaration ) { + /** + * The field declaration. + * + * @var Zoninator_REST_Field_Declaration $field_declaration + */ + if ( $field_declaration->get_kind() === $filter_by_type ) { + $filtered[] = $field_declaration; + } + } + return $filtered; + } + + /** + * Initialize_field_map + * + * @param array $declared_field_builders Array. + * + * @return array + */ + private static function initialize_field_map( $declared_field_builders ) { + $fields = array(); + foreach ( $declared_field_builders as $field_builder ) { + /** + * Builder + * + * @var Zoninator_REST_Field_Declaration $field Field Builder. + */ + $field = $field_builder->build(); + $fields[ $field->get_name() ] = $field; + } + return $fields; + } + + /** + * Get this model's data store + * + * @return Zoninator_REST_Interfaces_Data_Store + */ + public function get_data_store() { + $class_name = get_class( $this ); + if ( ! isset( self::$data_stores_by_class_name[ $class_name ] ) ) { + self::$data_stores_by_class_name[ $class_name ] = new Zoninator_REST_Data_Store_Nil(); + } + return self::$data_stores_by_class_name[ $class_name ]; + } + + /** + * Set this model's data store + * + * @param Zoninator_REST_Interfaces_Data_Store $data_store A builder or a Data store. + * @throws Zoninator_REST_Exception Throws when Data Store Invalid. + */ + public function with_data_store( $data_store ) { + $class_name = get_class( $this ); + // at this point we should have a data store. + Zoninator_REST_Expect::is_a( $data_store, 'Zoninator_REST_Interfaces_Data_Store' ); + self::$data_stores_by_class_name[ $class_name ] = $data_store; + } + + /** + * Get this model's environment + * + * @return Zoninator_REST_Environment|null + */ + public function get_environment() { + $class_name = get_class( $this ); + if ( isset( self::$environments_by_class_name[ $class_name ] ) ) { + return self::$environments_by_class_name[ $class_name ]; + } + return null; + } + + /** + * Set the model base class environment (change effective in all subclasses) + * + * @param Zoninator_REST_Environment $environment The Environment. + * + * @return Zoninator_REST_Interfaces_Model + * + * @throws Zoninator_REST_Exception If an Zoninator_REST_Environment is not provided. + */ + public function with_environment( $environment ) { + Zoninator_REST_Expect::is_a( $environment, 'Zoninator_REST_Environment' ); + $class_name = get_class( $this ); + self::$environments_by_class_name[ $class_name ] = $environment; + return $this; + } + + /** + * Create a new Model Instance + * + * @param array $data The data. + * @param array $args Args. + * + * @return Zoninator_REST_Interfaces_Model + * @throws Zoninator_REST_Exception Throws if data not an array. + */ + public function create( $data, $args = array() ) { + Zoninator_REST_Expect::that( is_array( $data ), '$data should be an array' ); + Zoninator_REST_Expect::that( is_array( $args ), '$args should be an array' ); + $class_name = get_class( $this ); + return new $class_name( $data, $args ); + } + + /** + * Merge values from array with current values. + * Note: Values change in place. + * + * @param array $data The data (key-value assumed). + * @param bool $updating Is this an update?. + * + * @return Zoninator_REST_Interfaces_Model|WP_Error + * @throws Zoninator_REST_Exception Throws. + */ + function update_from_array( $data, $updating = false ) { + $mapped_data = self::map_data( $data, $updating ); + foreach ( $mapped_data as $name => $value ) { + $this->set( $name, $value ); + } + return $this->sanitize(); + } + + /** + * Creates a new Model From a Data Array + * + * @param array $data The Data. + * + * @return Zoninator_REST_Model|WP_Error + */ + public function new_from_array( $data ) { + $field_data = $this->map_data( $data, false ); + return $this->create( $field_data )->sanitize(); + } + + /** + * Get field DTO Mappings + * + * @return array + */ + public function get_dto_field_mappings() { + $mappings = array(); + foreach ( $this->get_fields() as $field_declaration ) { + /** + * Declaration + * + * @var Zoninator_REST_Field_Declaration $field_declaration + */ + if ( ! $field_declaration->supports_output_type( 'json' ) ) { + continue; + } + $mappings[ $field_declaration->get_data_transfer_name() ] = $field_declaration->get_name(); + } + return $mappings; + } + + /** + * Prepare the Model for Data Transfer + * + * @return array + */ + function to_dto() { + $result = array(); + foreach ( $this->get_dto_field_mappings() as $mapping_name => $field_name ) { + $value = $this->get( $field_name ); + $result[ $mapping_name ] = $value; + } + + return $result; + } + + /** + * Map data names + * + * @param array $data The data to map. + * @param bool $updating Are we Updating. + * @return array + */ + private function map_data( $data, $updating = false ) { + $request_data = array(); + $fields = $this->get_fields(); + foreach ( $fields as $field ) { + /** + * Field + * + * @var Zoninator_REST_Field_Declaration $field Field. + */ + if ( $field->is_kind( Zoninator_REST_Field_Declaration::DERIVED ) ) { + continue; + } + $dto_name = $field->get_data_transfer_name(); + $field_name = $field->get_name(); + if ( isset( $data[ $dto_name ] ) && ! ( $updating && $field->is_primary() ) ) { + $value = $data[ $dto_name ]; + $request_data[ $field_name ] = $value; + } + } + return $request_data; + } + + /** + * Call a method. + * + * @param string $method The method. + * @param array $args The args. + * + * @return mixed + * @throws Zoninator_REST_Exception Throw if method nonexistent. + */ + private function call( $method, $args = array() ) { + if ( is_callable( $method ) ) { + return call_user_func_array( $method, $args ); + } + Zoninator_REST_Expect::that( method_exists( $this, $method ), $method . ' does not exist' ); + return call_user_func_array( array( $this, $method ), $args ); + } + + /** + * Get name + * + * @return string + */ + public function get_name() { + return strtolower( get_class( $this ) ); + } + + /** + * Declare fields. + * + * @return array + */ + public function declare_fields() { + Zoninator_REST_Expect::should_override( __METHOD__ ); + return array(); + } + + /** + * Get the id + * + * @return mixed|null + */ + function get_id() { + return $this->get( 'id' ); + } + + /** + * Set the id + * + * @param mixed $new_id The new id. + * + * @return mixed|null + */ + function set_id( $new_id ) { + return $this->set( 'id', $new_id ); + } + + /** + * Create from Post. + * + * @param WP_Post $post Post. + * @return Zoninator_REST_Model + * @throws Zoninator_REST_Exception If something goes wrong. + */ + public static function from_raw_data( $post ) { + $raw_post_data = $post->to_array(); + $raw_meta_data = get_post_meta( $post->ID ); // assumes we are only ever adding one postmeta per key. + + $flattened_meta = array(); + foreach ( $raw_meta_data as $key => $value_arr ) { + $flattened_meta[ $key ] = $value_arr[0]; + } + $merged_data = array_merge( $raw_post_data, $flattened_meta ); + + return self::create( $merged_data, array( + 'deserialize' => true, + ) ); + } + + /** + * Transform raw data to model data + * + * @param array $data Data. + * @return array + */ + public function deserialize( $data ) { + $field_declarations = $this->get_fields(); + $raw_data = array(); + $post_array_keys = array_keys( $data ); + foreach ( $field_declarations as $declaration ) { + /** + * Declaration + * + * @var Zoninator_REST_Field_Declaration $declaration + */ + $key = $declaration->get_name(); + $mapping = $declaration->get_map_from(); + $value = null; + if ( in_array( $key, $post_array_keys, true ) ) { + // simplest case: we got a $key for this, so just map it. + $value = $this->deserialize_field( $declaration, $data[ $key ] ); + } elseif ( in_array( $mapping, $post_array_keys, true ) ) { + // other case: we got a mapping. + $value = $this->deserialize_field( $declaration, $data[ $mapping ] ); + } else { + // just provide a default. + $value = $declaration->get_default_value(); + } + $raw_data[ $key ] = $declaration->cast_value( $value ); + } + return $raw_data; + } + + /** + * Transform Model to raw data array + * + * @param null|string $field_type Type. + * + * @return array + */ + function serialize( $field_type = null ) { + $field_values_to_insert = array(); + foreach ( $this->get_fields( $field_type ) as $field_declaration ) { + /** + * Declaration + * + * @var Zoninator_REST_Field_Declaration $field_declaration + */ + $what_to_map_to = $field_declaration->get_map_from(); + $value = $this->get( $field_declaration->get_name() ); + $field_values_to_insert[ $what_to_map_to ] = $this->serialize_field( $field_declaration, $value ); + } + + return $field_values_to_insert; + } + + /** + * Deserialize + * + * @param Zoninator_REST_Field_Declaration $field_declaration Declaration. + * @param mixed $value Value. + * @return mixed the deserialized value + */ + private function deserialize_field( $field_declaration, $value ) { + $deserializer = $field_declaration->get_deserializer(); + if ( isset( $deserializer ) && ! empty( $deserializer ) ) { + return $this->call( $deserializer, array( $value ) ); + } + return $value; + } + + /** + * Serialize + * + * @param Zoninator_REST_Field_Declaration $field_declaration Declaration. + * @param mixed $value Value. + * @return mixed + * @throws Zoninator_REST_Exception If call fails. + */ + private function serialize_field( $field_declaration, $value ) { + $serializer = $field_declaration->get_serializer(); + if ( isset( $serializer ) && ! empty( $serializer ) ) { + return $this->call( $serializer, array( $value ) ); + } + return $value; + } + + /** + * Handle Permissions for a REST Controller Action + * + * @param WP_REST_Request $request The request. + * @param string $action The action (e.g. index, create update etc). + * @return bool + */ + public function permissions_check( $request, $action ) { + $class_name = get_class( $this ); + if ( isset( self::$permissions_providers_by_class_name[ $class_name ] ) ) { + $permissions_provider = self::$permissions_providers_by_class_name[ $class_name ]; + return call_user_func_array( array( $permissions_provider, 'permissions_check' ), array( $request, $action ) ); + } + return true; + } + + /** + * Set a Proxy Permission Provider for this class + * + * @param Zoninator_REST_Interfaces_Permissions_Provider $permissions_provider PP. + * @return Zoninator_REST_Model $this + */ + public function with_permissions_provider( $permissions_provider ) { + Zoninator_REST_Expect::is_a( $permissions_provider, 'Zoninator_REST_Interfaces_Permissions_Provider' ); + $class_name = get_class( $this ); + self::$permissions_providers_by_class_name[ $class_name ] = $permissions_provider; + return $this; + } +} diff --git a/lib/zoninator_rest/class-zoninator-rest-type.php b/lib/zoninator_rest/class-zoninator-rest-type.php new file mode 100644 index 0000000..afe0b38 --- /dev/null +++ b/lib/zoninator_rest/class-zoninator-rest-type.php @@ -0,0 +1,90 @@ +identifier = $identifier; + } + + /** + * The name + * + * @return string + */ + function name() { + return $this->identifier; + } + + /** + * The default value + * + * @return null + */ + function default_value() { + return null; + } + + /** + * Cast value to be Type + * + * @param mixed $value The value that needs casting. + * + * @return mixed + */ + function cast( $value ) { + return $value; + } + + /** + * Sanitize this value + * + * @param mixed $value The value to sanitize. + * + * @return mixed + */ + function sanitize( $value ) { + return $value; + } + + /** + * Get this type's JSON Schema. + * + * @return array + */ + function schema() { + return array( + 'type' => $this->name(), + ); + } + + /** + * Get our "Any" type + * + * @return Zoninator_REST_Type + */ + static function any() { + return new self( 'any' ); + } +} diff --git a/lib/zoninator_rest/controller/bundle/class-zoninator-rest-controller-bundle-builder.php b/lib/zoninator_rest/controller/bundle/class-zoninator-rest-controller-bundle-builder.php new file mode 100644 index 0000000..f940050 --- /dev/null +++ b/lib/zoninator_rest/controller/bundle/class-zoninator-rest-controller-bundle-builder.php @@ -0,0 +1,98 @@ +bundle = $bundle; + } + + /** + * Build it + * + * @return Zoninator_REST_Interfaces_Controller_Bundle + */ + public function build() { + if ( is_a( $this->bundle, 'Zoninator_REST_Interfaces_Controller_Bundle' ) ) { + return $this->bundle; + } + return new Zoninator_REST_Controller_Bundle( $this->bundle_prefix, $this->endpoint_builders ); + } + + /** + * Prefix. + * + * @param string $bundle_prefix Prefix. + * @return Zoninator_REST_Controller_Bundle_Builder $this + */ + public function with_prefix( $bundle_prefix ) { + $this->bundle_prefix = $bundle_prefix; + return $this; + } + + /** + * Env. + * + * @param Zoninator_REST_Environment $env Env. + * @return Zoninator_REST_Controller_Bundle_Builder $this + */ + public function with_environment( $env ) { + $this->environment = $env; + return $this; + } + + /** + * Endpoint. + * + * Adds a new Zoninator_REST_Controller_Builder to our builders and returns it for further setup. + * + * @param null|Zoninator_REST_Interfaces_Controller $controller_object The (optional) controller object. + * @return Zoninator_REST_Controller_Bundle_Builder $this + */ + public function add_endpoint( $controller_object = null ) { + Zoninator_REST_Expect::is_a( $controller_object, 'Zoninator_REST_Interfaces_Controller' ); + $this->endpoint_builders[] = $controller_object; + return $this; + } +} diff --git a/lib/zoninator_rest/controller/class-zoninator-rest-controller-action.php b/lib/zoninator_rest/controller/class-zoninator-rest-controller-action.php new file mode 100644 index 0000000..a26c876 --- /dev/null +++ b/lib/zoninator_rest/controller/class-zoninator-rest-controller-action.php @@ -0,0 +1,166 @@ + WP_REST_Server::READABLE, + 'show' => WP_REST_Server::READABLE, + 'create' => WP_REST_Server::CREATABLE, + 'update' => WP_REST_Server::EDITABLE, + 'delete' => WP_REST_Server::DELETABLE, + 'any' => WP_REST_Server::ALLMETHODS, + ); + + /** + * The action name + * + * @var string + */ + private $action_name; + + /** + * The Handler + * + * @var null|array|string + */ + private $handler; + + /** + * The Permissions Callback + * + * @var null|array|string + */ + private $permission_callback; + + /** + * The Args + * + * @var null|array|string + */ + private $args; + + /** + * Zoninator_REST_Controller_Action constructor. + * + * @param Zoninator_REST_Controller $controller Controller. + * @param string $action_name The action Name. + */ + public function __construct( $controller, $action_name ) { + $is_known_action = in_array( $action_name, array_keys( $this->actions_to_http_methods ), true ); + Zoninator_REST_Expect::that( $is_known_action, 'Unknown method: ' . $action_name ); + + $this->controller = $controller; + $this->action_name = $action_name; + $this->handler = null; + $this->args = null; + $this->permission_callback = null; + } + + /** + * Get Name + * + * @return string + */ + public function name() { + return $this->action_name; + } + + /** + * Set Permissions + * + * @param mixed $callable A Callable. + * + * @return Zoninator_REST_Controller_Action + */ + public function permissions( $callable ) { + $this->permission_callback = $callable; + return $this; + } + + /** + * Set Handler + * + * @param mixed $callable A Callable. + * + * @return Zoninator_REST_Controller_Action + */ + public function callback( $callable ) { + $this->handler = $callable; + return $this; + } + + /** + * Set Handler + * + * @param mixed $callable A Callable. + * + * @return Zoninator_REST_Controller_Action + */ + public function args( $callable ) { + $this->args = $callable; + return $this; + } + + /** + * Used in register rest route + * + * @return array + */ + public function as_array() { + $callable_func = $this->expect_callable( $this->handler ); + if ( null !== $this->permission_callback ) { + $permission_callback = $this->expect_callable( $this->permission_callback ); + } else { + $permission_callback = $this->expect_callable( array( $this->controller, $this->action_name . '_permissions_check' ) ); + } + + if ( null !== $this->args ) { + $args = call_user_func( $this->expect_callable( $this->args ), $this->actions_to_http_methods[ $this->action_name ] ); + } else { + $args = $this->controller->get_endpoint_args_for_item_schema( $this->actions_to_http_methods[ $this->action_name ] ); + } + + $result = array( + 'methods' => $this->actions_to_http_methods[ $this->action_name ], + 'callback' => $callable_func, + 'permission_callback' => $permission_callback, + 'args' => $args, + ); + + return $result; + } + + /** + * Expect a callable + * + * @param mixed $callable_func A Callable. + * @return array + * @throws Zoninator_REST_Exception If not a callable. + */ + private function expect_callable( $callable_func ) { + if ( ! is_callable( $callable_func ) ) { + // Check if controller has a public method called $callable_func. + if ( is_string( $callable_func ) && method_exists( $this->controller, $callable_func ) ) { + return array( $this->controller, $callable_func ); + } + Zoninator_REST_Expect::that( is_callable( $callable_func ), 'Callable Expected: $callable_func' ); + } + return $callable_func; + } +} diff --git a/lib/zoninator_rest/controller/class-zoninator-rest-controller-bundle.php b/lib/zoninator_rest/controller/class-zoninator-rest-controller-bundle.php new file mode 100644 index 0000000..328ed64 --- /dev/null +++ b/lib/zoninator_rest/controller/class-zoninator-rest-controller-bundle.php @@ -0,0 +1,99 @@ +prefix = $bundle_prefix; + $this->endpoints = $endpoints; + } + + /** + * Register this bundle with the environment. + * + * @param Zoninator_REST_Environment $environment The Environment. + * @return Zoninator_REST_Controller_Bundle $this + * @throws Zoninator_REST_Exception When no prefix is defined. + */ + function register( $environment ) { + Zoninator_REST_Expect::that( null !== $this->prefix, 'prefix should be defined' ); + $this->environment = $environment; + /** + * Add/remove endpoints. Useful for extensions + * + * @param array $endpoints An array of Zoninator_REST_Interfaces_Controller + * @param $bundle Zoninator_REST_Controller_Bundle The bundle instance. + * + * @return array + */ + $endpoints = (array) apply_filters( + 'mt_rest_api_controller_bundle_get_endpoints', + $this->endpoints, + $this + ); + + foreach ( $endpoints as $endpoint ) { + /** + * Controller + * + * @var Zoninator_REST_Interfaces_Controller + */ + $endpoint->register( $this, $this->environment ); + } + + return $this; + } + + /** + * Get Prefix. + * + * @return string + */ + function get_prefix() { + return $this->prefix; + } +} + diff --git a/lib/zoninator_rest/controller/class-zoninator-rest-controller-crud.php b/lib/zoninator_rest/controller/class-zoninator-rest-controller-crud.php new file mode 100644 index 0000000..0ecabe1 --- /dev/null +++ b/lib/zoninator_rest/controller/class-zoninator-rest-controller-crud.php @@ -0,0 +1,199 @@ +add_route( '/' ) + ->add_action( $this->action( 'index', array( $this, 'get_items' ) ) ) + ->add_action( $this->action( 'create', array( $this, 'create_item' ) ) ); + + $this->add_route( '/(?P\d+)' ) + ->add_action( $this->action( 'show', array( $this, 'get_item' ) ) ) + ->add_action( $this->action( 'update', array( $this, 'update_item' ) ) ) + ->add_action( $this->action( 'delete', array( $this, 'delete_item' ) ) ); + } + + /** + * Get Items. + * + * @param WP_REST_Request $request Request. + * @return WP_REST_Response + */ + public function get_items( $request ) { + $item_id = isset( $request['id'] ) ? absint( $request['id'] ) : null; + + if ( null === $item_id ) { + $models = $this->get_model_data_store()->get_entities(); + $data = $this->prepare_dto( $models ); + return $this->ok( $data ); + } + + $model = $this->model_prototype->get_data_store()->get_entity( $item_id ); + if ( empty( $model ) ) { + return $this->not_found( __( 'Model not found' ) ); + } + + return $this->ok( $this->prepare_dto( $model ) ); + } + + /** + * Get Item + * + * @param WP_REST_Request $request Request. + * @return WP_REST_Response + */ + public function get_item( $request ) { + return $this->get_items( $request ); + } + + + /** + * Create Item + * + * @param WP_REST_Request $request Request. + * @return WP_REST_Response + */ + public function create_item( $request ) { + $is_update = false; + return $this->create_or_update( $request, $is_update ); + } + + /** + * Update Item + * + * @param WP_REST_Request $request Request. + * @return WP_REST_Response + */ + public function update_item( $request ) { + $is_update = true; + return $this->create_or_update( $request, $is_update ); + } + + /** + * Create Or Update Item + * + * @param WP_REST_Request $request Request. + * @param bool $is_update Is Update. + * + * @return WP_REST_Response + */ + protected function create_or_update( $request, $is_update = false ) { + $model_to_update = null; + if ( $is_update ) { + $id = isset( $request['id'] ) ? absint( $request['id'] ) : null; + if ( ! empty( $id ) ) { + $model_to_update = $this->model_prototype->get_data_store()->get_entity( $id ); + if ( empty( $model_to_update ) ) { + return $this->not_found( 'Model does not exist' ); + } + } + } + + if ( $is_update && $model_to_update ) { + $model = $model_to_update->update_from_array( $request->get_params(), $is_update ); + } else { + $model = $this->get_model_prototype()->new_from_array( $request->get_params() ); + } + + if ( is_wp_error( $model ) ) { + $wp_err = $model; + return $this->bad_request( $wp_err ); + } + + $validation = $model->validate(); + if ( is_wp_error( $validation ) ) { + return $this->bad_request( $validation ); + } + + $id_or_error = $this->model_data_store->upsert( $model ); + + if ( is_wp_error( $id_or_error ) ) { + return $this->bad_request( $id_or_error ); + } + + $dto = $this->prepare_dto( array( + 'id' => absint( $id_or_error ), + ) ); + + return $is_update ? $this->ok( $dto ) : $this->created( $dto ); + } + + /** + * Delete an Item + * + * @param WP_REST_Request $request Request. + * @return WP_REST_Response + */ + public function delete_item( $request ) { + $id = isset( $request['id'] ) ? absint( $request['id'] ) : null; + if ( empty( $id ) ) { + return $this->bad_request( 'No Model ID provided' ); + } + $model = $this->model_prototype->get_data_store()->get_entity( $id ); + if ( null === $model ) { + return $this->not_found( 'Model does not exist' ); + } + $result = $this->model_data_store->delete( $model ); + return $this->ok( $result ); + } + + /** + * Model To Dto + * + * @param Zoninator_REST_Interfaces_Model $model The Model. + * @return array + */ + protected function model_to_dto( $model ) { + $result = parent::model_to_dto( $model ); + $result['_links'] = $this->add_links( $model ); + return $result; + } + + /** + * Add Links + * + * @param Zoninator_REST_Interfaces_Model $model Model. + * @return array + */ + protected function add_links( $model ) { + $base_url = rest_url() . $this->controller_bundle->get_prefix() . $this->base . '/'; + + $result = array( + 'collection' => array( + array( + 'href' => esc_url( $base_url ), + ), + ), + ); + if ( $model->get_id() ) { + $result['self'] = array( + array( + 'href' => esc_url( $base_url . $model->get_id() ), + ), + ); + } + if ( $model->has( 'author' ) ) { + $result['author'] = array( + array( + 'href' => esc_url( rest_url() . 'wp/v2/users/' . $model->get( 'author' ) ), + ), + ); + } + return $result; + } +} diff --git a/lib/zoninator_rest/controller/class-zoninator-rest-controller-extension.php b/lib/zoninator_rest/controller/class-zoninator-rest-controller-extension.php new file mode 100644 index 0000000..5835429 --- /dev/null +++ b/lib/zoninator_rest/controller/class-zoninator-rest-controller-extension.php @@ -0,0 +1,83 @@ +model_definition_name = $model_definition_name; + $this->object_to_extend = $object_to_extend; + } + + /** + * Register This Controller + * + * @param Zoninator_REST_Environment $environment The Environment to use. + * @throws Zoninator_REST_Exception Throws. + * + * @return bool|WP_Error true if valid otherwise error. + */ + function register( $environment ) { + $this->environment = $environment; + $this->model_definition = $this->environment->model( $this->model_definition_name ); + if ( ! $this->model_definition ) { + return new WP_Error( 'model-not-found' ); + } + $fields = $this->model_definition->get_fields(); + foreach ( $fields as $field ) { + $this->register_field( $field ); + } + + return true; + } + + /** + * Register Field + * + * @param Zoninator_REST_Field_Declaration $field Field. + */ + private function register_field( $field ) { + register_rest_field( $this->object_to_extend, $field->get_data_transfer_name(), array( + 'get_callback' => $field->get_reader(), + 'update_callback' => $field->get_updater(), + 'schema' => $field->as_item_schema_property(), + ) ); + } +} diff --git a/lib/zoninator_rest/controller/class-zoninator-rest-controller-model.php b/lib/zoninator_rest/controller/class-zoninator-rest-controller-model.php new file mode 100644 index 0000000..2bcaf7b --- /dev/null +++ b/lib/zoninator_rest/controller/class-zoninator-rest-controller-model.php @@ -0,0 +1,165 @@ +base = $base; + $this->model_class_name = $model_class_name; + } + + /** + * Get our model factory + * + * @return Zoninator_REST_Model + */ + protected function get_model_prototype() { + return $this->model_prototype; + } + + /** + * Register this controller, initialize model-related object fields. + * + * @param Zoninator_REST_Controller_Bundle $bundle The bundle to use. + * @param Zoninator_REST_Environment $environment The Environment. + * + * @throws Zoninator_REST_Exception If an invalid model is provided. + * + * @return bool|WP_Error true if valid otherwise error. + */ + public function register( $bundle, $environment ) { + $this->model_prototype = $environment->model( $this->model_class_name ); + $this->model_data_store = $this->model_prototype->get_data_store(); + return parent::register( $bundle, $environment ); + } + + /** + * Retrieves the item's schema, conforming to JSON Schema. + * + * In our case, it gets fields/types from our definition's declared fields. + * + * @access public + * + * @return array Item schema data. + */ + public function get_item_schema() { + $model_definition = $this->get_model_prototype(); + $fields = $model_definition->get_fields(); + $properties = array(); + $required = array(); + foreach ( $fields as $field_declaration ) { + /** + * Our declaration + * + * @var Zoninator_REST_Field_Declaration $field_declaration + */ + $properties[ $field_declaration->get_data_transfer_name() ] = $field_declaration->as_item_schema_property(); + if ( $field_declaration->is_required() ) { + $required[] = $field_declaration->get_data_transfer_name(); + } + } + $schema = array( + '$schema' => 'http://json-schema.org/schema#', + 'title' => $model_definition->get_name(), + 'type' => 'object', + 'properties' => (array) apply_filters( 'mixtape_rest_api_schema_properties', $properties, $this->get_model_prototype() ), + ); + + if ( ! empty( $required ) ) { + $schema['required'] = $required; + } + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get Model DataStore + * + * @return Zoninator_REST_Interfaces_Data_Store + */ + protected function get_model_data_store() { + return $this->model_data_store; + } + + /** + * Generic Permissions Check. + * + * @param WP_REST_Request $request Request. + * @param string $action One of (index, show, create, update, delete). + * @return bool + */ + public function permissions_check( $request, $action = 'any' ) { + return $this->get_model_prototype()->permissions_check( $request, $action ); + } + + /** + * Prepare Entity to be a DTO + * + * @param array|Zoninator_REST_Model_Collection|Zoninator_REST_Interfaces_Model $entity The Entity. + * @return array + */ + protected function prepare_dto( $entity ) { + if ( is_a( $entity, 'Zoninator_REST_Model_Collection' ) ) { + $results = array(); + foreach ( $entity->get_items() as $model ) { + $results[] = $this->model_to_dto( $model ); + } + return $results; + } + + if ( is_a( $entity, 'Zoninator_REST_Interfaces_Model' ) ) { + return $this->model_to_dto( $entity ); + } + + return $entity; + } + + /** + * Map a model to a Data Transfer Object (plain array) + * + * @param Zoninator_REST_Interfaces_Model $model The Model. + * @return array + */ + protected function model_to_dto( $model ) { + return $model->to_dto(); + } +} diff --git a/lib/zoninator_rest/controller/class-zoninator-rest-controller-route.php b/lib/zoninator_rest/controller/class-zoninator-rest-controller-route.php new file mode 100644 index 0000000..6176100 --- /dev/null +++ b/lib/zoninator_rest/controller/class-zoninator-rest-controller-route.php @@ -0,0 +1,90 @@ +controller = $controller; + $this->pattern = $pattern; + $this->actions = array(); + $this->http_methods = explode( ', ', WP_REST_Server::ALLMETHODS ); + } + + /** + * Add/Get an action + * + * @param Zoninator_REST_Controller_Action $action Action. + * + * @return Zoninator_REST_Controller_Route + */ + public function add_action( $action ) { + $this->actions[ $action->name() ] = $action; + return $this; + } + + /** + * Gets Route info to use in Register rest route. + * + * @throws Zoninator_REST_Exception If invalid callable. + * @return array + */ + public function as_array() { + $result = array(); + $result['pattern'] = $this->pattern; + $result['actions'] = array(); + foreach ( $this->actions as $action => $route_action ) { + /** + * The route action. + * + * @var Zoninator_REST_Controller_Action $route_action + */ + $result['actions'][] = $route_action->as_array(); + } + return $result; + } +} diff --git a/lib/zoninator_rest/controller/class-zoninator-rest-controller-settings.php b/lib/zoninator_rest/controller/class-zoninator-rest-controller-settings.php new file mode 100644 index 0000000..3194464 --- /dev/null +++ b/lib/zoninator_rest/controller/class-zoninator-rest-controller-settings.php @@ -0,0 +1,86 @@ +add_route() + ->add_action( $this->action( 'index', array( $this, 'get_items' ) ) ) + ->add_action( $this->action( 'update', array( $this, 'create_item' ) ) ); + } + + /** + * Get Settings + * + * @param WP_REST_Request $request The request. + * @return WP_REST_Response + */ + public function get_items( $request ) { + $model = $this->model_prototype->get_data_store()->get_entity( null ); + if ( empty( $model ) ) { + return $this->not_found( __( 'Settings not found' ) ); + } + + return $this->ok( $this->prepare_dto( $model ) ); + } + + /** + * Create or Update settings. + * + * @param WP_REST_Request $request Request. + * @return WP_REST_Response + */ + public function create_item( $request ) { + return $this->create_or_update( $request ); + } + + /** + * Create or Update a Model + * + * @param WP_REST_Request $request Request. + * @return WP_REST_Response + */ + protected function create_or_update( $request ) { + $is_update = $request->get_method() !== 'POST'; + $model_to_update = $this->model_prototype->get_data_store()->get_entity( null ); + if ( empty( $model_to_update ) ) { + return $this->not_found( 'Model does not exist' ); + } + + $model = $model_to_update->update_from_array( $request->get_params(), true ); + + if ( is_wp_error( $model ) ) { + return $this->bad_request( $model ); + } + + $validation = $model->validate(); + if ( is_wp_error( $validation ) ) { + return $this->bad_request( $validation ); + } + + $id_or_error = $this->model_data_store->upsert( $model ); + + if ( is_wp_error( $id_or_error ) ) { + return $this->bad_request( $id_or_error ); + } + + $model = $this->model_prototype->get_data_store()->get_entity( null ); + $dto = $this->prepare_dto( $model ); + + return $is_update ? $this->ok( $dto ) : $this->created( $dto ); + } +} diff --git a/lib/zoninator_rest/data/class-zoninator-rest-data-mapper.php b/lib/zoninator_rest/data/class-zoninator-rest-data-mapper.php new file mode 100644 index 0000000..08ea6bc --- /dev/null +++ b/lib/zoninator_rest/data/class-zoninator-rest-data-mapper.php @@ -0,0 +1,94 @@ +definition = $definition; + $this->serializer = $serializer; + } + + /** + * Transform raw data to model data + * + * @param array $data Data. + * @param array $field_declarations Declarations. + * @return array + */ + function raw_data_to_model_data( $data, $field_declarations ) { + $raw_data = array(); + $post_array_keys = array_keys( $data ); + foreach ( $field_declarations as $declaration ) { + /** + * Declaration + * + * @var Zoninator_REST_Field_Declaration $declaration + */ + $key = $declaration->get_name(); + $mapping = $declaration->get_map_from(); + $value = null; + if ( in_array( $key, $post_array_keys, true ) ) { + // simplest case: we got a $key for this, so just map it. + $value = $this->serializer->deserialize( $declaration, $data[ $key ] ); + } elseif ( in_array( $mapping, $post_array_keys, true ) ) { + $value = $this->serializer->deserialize( $declaration, $data[ $mapping ] ); + } else { + $value = $declaration->get_default_value(); + } + $raw_data[ $key ] = $declaration->cast_value( $value ); + } + return $raw_data; + } + + /** + * Transform Model to raw data array + * + * @param Zoninator_REST_Interfaces_Model $model Model. + * @param null|string $field_type Type. + * @return array + */ + function model_to_data( $model, $field_type = null ) { + $field_values_to_insert = array(); + foreach ( $this->definition->get_field_declarations( $field_type ) as $field_declaration ) { + /** + * Declaration + * + * @var Zoninator_REST_Field_Declaration $field_declaration + */ + $what_to_map_to = $field_declaration->get_map_from(); + $value = $model->get( $field_declaration->get_name() ); + $field_values_to_insert[ $what_to_map_to ] = $this->serializer->serialize( $field_declaration, $value ); + } + + return $field_values_to_insert; + } +} diff --git a/lib/zoninator_rest/data/class-zoninator-rest-data-serializer.php b/lib/zoninator_rest/data/class-zoninator-rest-data-serializer.php new file mode 100644 index 0000000..52dc711 --- /dev/null +++ b/lib/zoninator_rest/data/class-zoninator-rest-data-serializer.php @@ -0,0 +1,59 @@ +model_declaration = $model_definition->get_model_declaration(); + } + + /** + * Deserialize + * + * @param Zoninator_REST_Field_Declaration $field_declaration Declaration. + * @param mixed $value Value. + * @return mixed the deserialized value + */ + function deserialize( $field_declaration, $value ) { + $deserializer = $field_declaration->get_deserializer(); + return $deserializer ? $this->model_declaration->call( $deserializer, array( $value ) ) : $value; + } + + /** + * Serialize + * + * @param Zoninator_REST_Field_Declaration $field_declaration Declaration. + * @param mixed $value Value. + * @return mixed + * @throws Zoninator_REST_Exception If call fails. + */ + function serialize( $field_declaration, $value ) { + $serializer = $field_declaration->get_serializer(); + if ( isset( $serializer ) && ! empty( $serializer ) ) { + return $this->model_declaration->call( $serializer, array( $value ) ); + } + return $value; + } +} diff --git a/lib/zoninator_rest/data/store/class-zoninator-rest-data-store-abstract.php b/lib/zoninator_rest/data/store/class-zoninator-rest-data-store-abstract.php new file mode 100644 index 0000000..0ad89b1 --- /dev/null +++ b/lib/zoninator_rest/data/store/class-zoninator-rest-data-store-abstract.php @@ -0,0 +1,72 @@ +type_serializers = array(); + $this->args = $args; + Zoninator_REST_Expect::is_a( $model_prototype, 'Zoninator_REST_Interfaces_Model' ); + $this->set_model_factory( $model_prototype ); + } + + /** + * Set Definition + * + * @param Zoninator_REST_Model $factory Def. + * + * @return Zoninator_REST_Interfaces_Data_Store $this + */ + private function set_model_factory( $factory ) { + $this->model_prototype = $factory; + $this->configure(); + return $this; + } + + /** + * Configure + */ + protected function configure() { + } + + /** + * Get Definition + * + * @return Zoninator_REST_Model + */ + public function get_model_prototype() { + return $this->model_prototype; + } +} diff --git a/lib/zoninator_rest/data/store/class-zoninator-rest-data-store-builder.php b/lib/zoninator_rest/data/store/class-zoninator-rest-data-store-builder.php new file mode 100644 index 0000000..dc6fc89 --- /dev/null +++ b/lib/zoninator_rest/data/store/class-zoninator-rest-data-store-builder.php @@ -0,0 +1,85 @@ +store_class = $data_store_class; + return $this; + } + + /** + * Set Args + * + * @param array $args Args. + * @return Zoninator_REST_Data_Store_Builder $this + */ + function with_args( $args ) { + $this->args = $args; + return $this; + } + + /** + * Set Model Definition + * + * @param string|Zoninator_REST_Model_Definition $model_definition Def. + * @return Zoninator_REST_Data_Store_Builder $this + */ + function with_model_definition( $model_definition ) { + $this->model_definition = $model_definition; + return $this; + } + + /** + * Build + * + * @return Zoninator_REST_Interfaces_Data_Store + */ + function build() { + $store_class = $this->store_class; + return new $store_class( $this->model_definition, $this->args ); + } +} diff --git a/lib/zoninator_rest/data/store/class-zoninator-rest-data-store-customposttype.php b/lib/zoninator_rest/data/store/class-zoninator-rest-data-store-customposttype.php new file mode 100644 index 0000000..f05fc19 --- /dev/null +++ b/lib/zoninator_rest/data/store/class-zoninator-rest-data-store-customposttype.php @@ -0,0 +1,178 @@ +post_type = isset( $args['post_type'] ) ? $args['post_type'] : 'post'; + parent::__construct( $model_prototype, $args ); + } + + /** + * Get Entities + * + * @param null|mixed $filter Filter. + * + * @return Zoninator_REST_Model_Collection + */ + public function get_entities( $filter = null ) { + $query = new WP_Query( array( + 'post_type' => $this->post_type, + 'post_status' => 'any', + ) ); + $posts = $query->get_posts(); + $collection = array(); + foreach ( $posts as $post ) { + $collection[] = $this->create_from_post( $post ); + } + return new Zoninator_REST_Model_Collection( $collection ); + } + + /** + * Get Entity + * + * @param int $id The id of the entity. + * @return Zoninator_REST_Model|null + */ + public function get_entity( $id ) { + $post = get_post( absint( $id ) ); + if ( empty( $post ) || $post->post_type !== $this->post_type ) { + return null; + } + + return $this->create_from_post( $post ); + } + + /** + * Create from Post. + * + * @param WP_Post $post Post. + * @return Zoninator_REST_Model + * @throws Zoninator_REST_Exception If something goes wrong. + */ + private function create_from_post( $post ) { + $field_declarations = $this->get_model_prototype()->get_fields(); + $raw_post_data = $post->to_array(); + $raw_meta_data = get_post_meta( $post->ID ); // assumes we are only ever adding one postmeta per key. + + $flattened_meta = array(); + foreach ( $raw_meta_data as $key => $value_arr ) { + $flattened_meta[ $key ] = $value_arr[0]; + } + $merged_data = array_merge( $raw_post_data, $flattened_meta ); + + return $this->get_model_prototype()->create( $merged_data, array( + 'deserialize' => true, + ) ); + } + + /** + * Delete + * + * @param Zoninator_REST_Interfaces_Model $model Model. + * @param array $args Args. + * @return mixed + */ + public function delete( $model, $args = array() ) { + $id = $model->get_id(); + + $args = wp_parse_args( $args, array( + 'force_delete' => false, + ) ); + + do_action( 'mixtape_data_store_delete_model_before', $model, $id ); + + if ( $args['force_delete'] ) { + $result = wp_delete_post( $model->get_id() ); + $model->set( 'id', 0 ); + do_action( 'mixtape_data_store_delete_model', $model, $id ); + } else { + $result = wp_trash_post( $model->get_id() ); + $model->set( 'status', 'trash' ); + do_action( 'mixtape_data_store_trash_model', $model, $id ); + } + + if ( false === $result ) { + do_action( 'mixtape_data_store_delete_model_fail', $model, $id ); + return new WP_Error( 'delete-failed', 'delete-failed' ); + } + return $result; + } + + /** + * Upsert + * + * @param Zoninator_REST_Interfaces_Model $model Model. + * + * @return mixed|WP_Error + */ + public function upsert( $model ) { + $id = $model->get_id(); + $updating = ! empty( $id ); + $fields = $model->serialize( Zoninator_REST_Field_Declaration::FIELD ); + $meta_fields = $model->serialize( Zoninator_REST_Field_Declaration::META ); + if ( ! isset( $fields['post_type'] ) ) { + $fields['post_type'] = $this->post_type; + } + if ( isset( $fields['ID'] ) && empty( $fields['ID'] ) ) { + // ID of 0 is not acceptable on CPTs, so remove it. + unset( $fields['ID'] ); + } + + do_action( 'mixtape_data_store_model_upsert_before', $model ); + + $id_or_error = wp_insert_post( $fields, true ); + if ( is_wp_error( $id_or_error ) ) { + do_action( 'mixtape_data_store_model_upsert_error', $model ); + return $id_or_error; + } + $model->set( 'id', absint( $id_or_error ) ); + foreach ( $meta_fields as $meta_key => $meta_value ) { + if ( $updating ) { + $id_or_bool = update_post_meta( $id_or_error, $meta_key, $meta_value ); + } else { + $id_or_bool = add_post_meta( $id_or_error, $meta_key, $meta_value ); + } + + if ( false === $id_or_bool ) { + do_action( 'mixtape_data_store_model_upsert_error', $model ); + // Something was wrong with this update/create. TODO: Should we stop mid create/update? + return new WP_Error( + 'mixtape-error-creating-meta', + 'There was an error updating/creating an entity field', + array( + 'field_key' => $meta_key, + 'field_value' => $meta_value, + ) + ); + } + } + + do_action( 'mixtape_data_store_model_upsert_after', $model ); + + return absint( $id_or_error ); + } +} diff --git a/lib/zoninator_rest/data/store/class-zoninator-rest-data-store-nil.php b/lib/zoninator_rest/data/store/class-zoninator-rest-data-store-nil.php new file mode 100644 index 0000000..81a2281 --- /dev/null +++ b/lib/zoninator_rest/data/store/class-zoninator-rest-data-store-nil.php @@ -0,0 +1,66 @@ +does_not_exist_guard = new stdClass(); + } + + /** + * Get Entities + * + * @param null|mixed $filter Filter. + * @return Zoninator_REST_Interfaces_Model + */ + public function get_entities( $filter = null ) { + // there is only one option bag and one option bag global per data store. + return $this->get_entity( null ); + } + + /** + * Get Entity + * + * @param int $id The id of the entity. + * @return Zoninator_REST_Interfaces_Model + */ + public function get_entity( $id ) { + $field_declarations = $this->get_model_prototype()->get_fields(); + $raw_data = array(); + foreach ( $field_declarations as $field_declaration ) { + /** + * Field Declaration + * + * @var Zoninator_REST_Field_Declaration $field_declaration + */ + $option = get_option( $field_declaration->get_map_from(), $this->does_not_exist_guard ); + if ( $this->does_not_exist_guard !== $option ) { + $raw_data[ $field_declaration->get_map_from() ] = $option; + } + } + + return $this->get_model_prototype()->create( $raw_data, array( + 'deserialize' => true, + ) ); + } + + /** + * Delete + * + * @param Zoninator_REST_Interfaces_Model $model Model. + * @param array $args Args. + * @return mixed + */ + public function delete( $model, $args = array() ) { + $options_to_delete = array_keys( $model->serialize() ); + foreach ( $options_to_delete as $option_to_delete ) { + if ( false !== get_option( $option_to_delete, false ) ) { + $result = delete_option( $option_to_delete ); + if ( false === $result ) { + return new WP_Error( 'delete-option-failed' ); + } + } + } + return true; + } + + /** + * Update/Insert + * + * @param Zoninator_REST_Interfaces_Model $model Model. + * @return mixed + */ + public function upsert( $model ) { + $fields_for_insert = $model->serialize(); + foreach ( $fields_for_insert as $option_name => $option_value ) { + $previous_value = get_option( $option_name, $this->does_not_exist_guard ); + if ( $this->does_not_exist_guard !== $previous_value ) { + update_option( $option_name, $option_value ); + } else { + add_option( $option_name, $option_value ); + } + } + return true; + } +} diff --git a/lib/zoninator_rest/field/class-zoninator-rest-field-declaration.php b/lib/zoninator_rest/field/class-zoninator-rest-field-declaration.php new file mode 100644 index 0000000..50431d7 --- /dev/null +++ b/lib/zoninator_rest/field/class-zoninator-rest-field-declaration.php @@ -0,0 +1,434 @@ +field_kinds, true ) ) { + throw new Zoninator_REST_Exception( 'every field should have a kind (one of ' . implode( ',', $this->field_kinds ) . ')' ); + } + + $this->name = $args['name']; + $this->description = $this->value_or_default( $args, 'description', '' ); + + $this->kind = $args['kind']; + $this->type = $this->value_or_default( $args, 'type', Zoninator_REST_Type::any() ); + $this->choices = $this->value_or_default( $args, 'choices', null ); + $this->default_value = $this->value_or_default( $args, 'default_value' ); + + $this->map_from = $this->value_or_default( $args, 'map_from' ); + $this->data_transfer_name = $this->value_or_default( $args, 'data_transfer_name', $this->get_name() ); + + $this->primary = $this->value_or_default( $args, 'primary', false ); + $this->required = $this->value_or_default( $args, 'required', false ); + $this->supported_outputs = $this->value_or_default( $args, 'supported_outputs', array( 'json' ) ); + + $this->sanitizer = $this->value_or_default( $args, 'sanitizer' ); + $this->validations = $this->value_or_default( $args, 'validations', array() ); + + $this->serializer = $this->value_or_default( $args, 'serializer' ); + $this->deserializer = $this->value_or_default( $args, 'deserializer' ); + + $this->before_get = $this->value_or_default( $args, 'before_get' ); + $this->before_set = $this->value_or_default( $args, 'before_set' ); + + $this->reader = $this->value_or_default( $args, 'reader' ); + $this->updater = $this->value_or_default( $args, 'updater' ); + } + + /** + * Get possible choices if set + * + * @return null|array + */ + public function get_choices() { + return $this->choices; + } + + /** + * Get Sanitizer + * + * @return callable|null + */ + public function get_sanitizer() { + return $this->sanitizer; + } + + /** + * Value or Default + * + * @param array $args Args. + * @param string $name Name. + * @param mixed $default Default. + * @return null + */ + private function value_or_default( $args, $name, $default = null ) { + return isset( $args[ $name ] ) ? $args[ $name ] : $default; + } + + /** + * Is Kind + * + * @param string $kind The kind. + * @return bool + */ + public function is_kind( $kind ) { + if ( ! in_array( $kind, $this->field_kinds, true ) ) { + return false; + } + return $this->kind === $kind; + } + + /** + * Get default value + * + * @return mixed + */ + public function get_default_value() { + if ( isset( $this->default_value ) && ! empty( $this->default_value ) ) { + return ( is_array( $this->default_value ) && is_callable( $this->default_value ) ) ? call_user_func( $this->default_value ) : $this->default_value; + } + + return $this->type->default_value(); + } + + /** + * Cast a value + * + * @param mixed $value Val. + * @return mixed + */ + public function cast_value( $value ) { + return $this->type->cast( $value ); + } + + /** + * Supports this type of output. + * + * @param string $type Type. + * @return bool + */ + public function supports_output_type( $type ) { + return in_array( $type, $this->supported_outputs, true ); + } + + /** + * As Item Schema Property + * + * @return array + */ + public function as_item_schema_property() { + $schema = $this->type->schema(); + $schema['context'] = array( 'view', 'edit' ); + $schema['description'] = $this->get_description(); + + if ( $this->get_choices() ) { + $schema['enum'] = (array) $this->get_choices(); + } + return $schema; + } + + /** + * Get Map From + * + * @return null + */ + public function get_map_from() { + if ( isset( $this->map_from ) && ! empty( $this->map_from ) ) { + return $this->map_from; + } + + return $this->get_name(); + } + + /** + * Get Kind + * + * @return mixed + */ + public function get_kind() { + return $this->kind; + } + + /** + * Get Name + * + * @return mixed + */ + public function get_name() { + return $this->name; + } + + /** + * Is Primary + * + * @return bool + */ + public function is_primary() { + return (bool) $this->primary; + } + + /** + * Is Required + * + * @return bool + */ + public function is_required() { + return (bool) $this->required; + } + + /** + * Get Description + * + * @return string + */ + public function get_description() { + if ( isset( $this->description ) && ! empty( $this->description ) ) { + return $this->description; + } + $name = ucfirst( str_replace( '_', ' ', $this->get_name() ) ); + return $name; + } + + /** + * Get Dto name + * + * @return string + */ + public function get_data_transfer_name() { + return isset( $this->data_transfer_name ) ? $this->data_transfer_name : $this->get_name(); + } + + /** + * Get Validations + * + * @return array + */ + public function get_validations() { + return $this->validations; + } + + /** + * Get Before get + * + * @return callable|null + */ + public function before_get() { + return $this->before_get; + } + + /** + * Get Serializer + * + * @return callable|null + */ + public function get_serializer() { + return $this->serializer; + } + + /** + * Get Deserializer + * + * @return callable|null + */ + public function get_deserializer() { + return $this->deserializer; + } + + /** + * Get Type + * + * @return Zoninator_REST_Interfaces_Type + */ + function get_type() { + return $this->type; + } + + /** + * Before Set + * + * @return callable|null + */ + public function before_set() { + return $this->before_set; + } + + /** + * Get Reader + * + * @return callable|null + */ + public function get_reader() { + return $this->reader; + } + + /** + * Get Updater + * + * @return callable|null + */ + public function get_updater() { + return $this->updater; + } +} diff --git a/lib/zoninator_rest/field/declaration/class-zoninator-rest-field-declaration-builder.php b/lib/zoninator_rest/field/declaration/class-zoninator-rest-field-declaration-builder.php new file mode 100644 index 0000000..8fc4825 --- /dev/null +++ b/lib/zoninator_rest/field/declaration/class-zoninator-rest-field-declaration-builder.php @@ -0,0 +1,290 @@ +args = array( + 'name' => '', + 'kind' => Zoninator_REST_Field_Declaration::FIELD, + 'type' => Zoninator_REST_Type::any(), + 'required' => false, + 'map_from' => null, + + 'sanitizer' => null, + + 'serializer' => null, + 'deserializer' => null, + + 'default_value' => null, + 'data_transfer_name' => null, + 'supported_outputs' => array( 'json' ), + 'description' => null, + 'validations' => array(), + 'choices' => null, + 'contexts' => array( 'view', 'edit' ), + 'before_set' => null, + 'before_get' => null, + 'reader' => null, + 'updater' => null, + ); + } + + /** + * Build it + * + * @return Zoninator_REST_Field_Declaration + */ + public function build() { + return new Zoninator_REST_Field_Declaration( $this->args ); + } + + /** + * Default Value. + * + * @param mixed $default_value Default. + * @return Zoninator_REST_Field_Declaration_Builder + */ + public function with_default( $default_value ) { + return $this->with( 'default_value', $default_value ); + } + + /** + * With Name + * + * @param string $name Name. + * @return Zoninator_REST_Field_Declaration_Builder + */ + public function with_name( $name ) { + return $this->with( 'name', $name ); + } + + /** + * With Kind + * + * @param string $kind Kind. + * @return Zoninator_REST_Field_Declaration_Builder + */ + public function with_kind( $kind ) { + return $this->with( 'kind', $kind ); + } + + /** + * With Map From + * + * @param string $mapped_from Mapped From. + * @return Zoninator_REST_Field_Declaration_Builder + */ + public function with_map_from( $mapped_from ) { + return $this->with( 'map_from', $mapped_from ); + } + + /** + * With Sanitizer + * + * @param callable $sanitizer Sanitizer. + * @return Zoninator_REST_Field_Declaration_Builder + */ + public function with_sanitizer( $sanitizer ) { + $this->expect_is_callable( $sanitizer, __METHOD__ ); + return $this->with( 'sanitizer', $sanitizer ); + } + + /** + * With Serializer + * + * @param callable $serializer Serializer. + * @return Zoninator_REST_Field_Declaration_Builder + */ + public function with_serializer( $serializer ) { + return $this->with( 'serializer', $serializer ); + } + + /** + * With Deserializer + * + * @param callable $deserializer Deserializer. + * @return Zoninator_REST_Field_Declaration_Builder + */ + public function with_deserializer( $deserializer ) { + return $this->with( 'deserializer', $deserializer ); + } + + /** + * With Required + * + * @param bool $required Req. + * @return Zoninator_REST_Field_Declaration_Builder + */ + public function with_required( $required = true ) { + return $this->with( 'required', $required ); + + } + + /** + * With Supported Outputs + * + * @param array $supported_outputs Outputs. + * @return Zoninator_REST_Field_Declaration_Builder + */ + public function with_supported_outputs( $supported_outputs = array() ) { + return $this->with( 'supported_outputs', (array) $supported_outputs ); + } + + /** + * Set the type definition of this field declaration + * + * @param Zoninator_REST_Interfaces_Type $value_type Type. + * @return Zoninator_REST_Field_Declaration_Builder $this + * + * @throws Zoninator_REST_Exception When not a type. + */ + public function with_type( $value_type ) { + if ( ! is_a( $value_type, 'Zoninator_REST_Interfaces_Type' ) ) { + throw new Zoninator_REST_Exception( get_class( $value_type ) . ' is not a Mixtape_Interfaces_Type' ); + } + return $this->with( 'type', $value_type ); + } + + /** + * With Dto Name + * + * @param string $dto_name Dto Name. + * @return Zoninator_REST_Field_Declaration_Builder + */ + public function with_dto_name( $dto_name ) { + return $this->with( 'data_transfer_name', $dto_name ); + } + + /** + * With Description + * + * @param string $description Description. + * @return Zoninator_REST_Field_Declaration_Builder + */ + public function with_description( $description ) { + return $this->with( 'description', $description ); + } + + /** + * With Validations + * + * @param array|mixed $validations Validations. + * @return Zoninator_REST_Field_Declaration_Builder + */ + public function with_validations( $validations ) { + if ( is_callable( $validations ) || ! is_array( $validations ) ) { + $validations = array( $validations ); + } + return $this->with( 'validations', $validations ); + } + + /** + * Before Set + * + * @param callable $before_set Before set. + * @return Zoninator_REST_Field_Declaration_Builder + */ + public function with_before_set( $before_set ) { + return $this->with( 'before_set', $before_set ); + } + + /** + * Before Get + * + * @param callable $before_get Before get. + * @return Zoninator_REST_Field_Declaration_Builder + */ + public function with_before_get( $before_get ) { + return $this->with( 'before_get', $before_get ); + } + + /** + * Choices. + * + * @param array|mixed $choices Choices. + * + * @return $this|Zoninator_REST_Field_Declaration_Builder + */ + public function with_choices( $choices ) { + if ( empty( $choices ) ) { + return $this; + } + return $this->with( 'choices', is_array( $choices ) ? $choices : array( $choices ) ); + } + + /** + * Set + * + * @param string $name Name. + * @param mixed $value Value. + * @return Zoninator_REST_Field_Declaration_Builder $this + */ + private function with( $name, $value ) { + $this->args[ $name ] = $value; + return $this; + } + + /** + * Derived Field + * + * @param callable $func The func. + * + * @return Zoninator_REST_Field_Declaration_Builder + */ + public function derived( $func = null ) { + if ( $func ) { + $this->with_map_from( $func ); + } + return $this->with_kind( Zoninator_REST_Field_Declaration::DERIVED ); + } + + /** + * Set Updater + * + * @param callable $func Func. + * @return Zoninator_REST_Field_Declaration_Builder $this + * @throws Zoninator_REST_Exception When no callable. + */ + public function with_updater( $func ) { + return $this->with( 'updater', $func ); + } + + /** + * Set reader + * + * @param callable $func Func. + * @return Zoninator_REST_Field_Declaration_Builder $this + * @throws Zoninator_REST_Exception When no callable. + */ + public function with_reader( $func ) { + return $this->with( 'reader', $func ); + } + + /** + * Callable test + * + * @param callable|mixed $thing Thing to test. + * @param string $func The caller. + * + * @throws Zoninator_REST_Exception If not callable. + */ + private function expect_is_callable( $thing, $func ) { + Zoninator_REST_Expect::that( is_callable( $thing ), $func . ' Expected a callable' ); + } +} diff --git a/lib/zoninator_rest/interfaces/class-zoninator-rest-interfaces-builder.php b/lib/zoninator_rest/interfaces/class-zoninator-rest-interfaces-builder.php new file mode 100644 index 0000000..192f04c --- /dev/null +++ b/lib/zoninator_rest/interfaces/class-zoninator-rest-interfaces-builder.php @@ -0,0 +1,22 @@ +models = $models; + } + + /** + * Get the contents of this collection. + * + * @return Iterator + */ + public function get_items() { + return new ArrayIterator( $this->models ); + } +} diff --git a/lib/zoninator_rest/model/class-zoninator-rest-model-declaration.php b/lib/zoninator_rest/model/class-zoninator-rest-model-declaration.php new file mode 100644 index 0000000..c29dd74 --- /dev/null +++ b/lib/zoninator_rest/model/class-zoninator-rest-model-declaration.php @@ -0,0 +1,116 @@ +model_definition = $def; + return $this; + } + + /** + * Get definition. + * + * @return Zoninator_REST_Model_Definition + */ + function definition() { + return $this->model_definition; + } + + /** + * Declare fields + * + * @param Zoninator_REST_Environment $env The Environment. + * + * @return void + * @throws Zoninator_REST_Exception Override this. + */ + function declare_fields( $env ) { + throw new Zoninator_REST_Exception( 'Override me: ' . __FUNCTION__ ); + } + + /** + * Get the id + * + * @param Zoninator_REST_Interfaces_Model $model The model. + * + * @return mixed|null + */ + function get_id( $model ) { + return $model->get( 'id' ); + } + + /** + * Set the id + * + * @param Zoninator_REST_Interfaces_Model $model The model. + * @param mixed $new_id The new id. + * + * @return mixed|null + */ + function set_id( $model, $new_id ) { + return $model->set( 'id', $new_id ); + } + + /** + * Call a method. + * + * @param string $method The method. + * @param array $args The args. + * + * @return mixed + * @throws Zoninator_REST_Exception Throw if method nonexistent. + */ + function call( $method, $args = array() ) { + if ( is_callable( $method ) ) { + return $this->perform_call( $method, $args ); + } + Zoninator_REST_Expect::that( method_exists( $this, $method ), $method . ' does not exist' ); + return $this->perform_call( array( $this, $method ), $args ); + } + + /** + * Get name + * + * @return string + */ + function get_name() { + return strtolower( get_class( $this ) ); + } + + /** + * Perform call + * + * @param mixed $callable A Callable. + * @param array $args The args. + * + * @return mixed + */ + private function perform_call( $callable, $args ) { + return call_user_func_array( $callable, $args ); + } +} diff --git a/lib/zoninator_rest/model/class-zoninator-rest-model-definition.php b/lib/zoninator_rest/model/class-zoninator-rest-model-definition.php new file mode 100644 index 0000000..0bb2ff9 --- /dev/null +++ b/lib/zoninator_rest/model/class-zoninator-rest-model-definition.php @@ -0,0 +1,339 @@ +field_declarations = null; + $this->environment = $environment; + $this->model_declaration = $model_declaration; + $this->model_class = get_class( $model_declaration ); + $this->permissions_provider = $permissions_provider; + $this->name = strtolower( $this->model_class ); + + $this->set_data_store( $data_store ); + } + + /** + * Get Model Class + * + * @return string + */ + function get_model_class() { + return $this->model_class; + } + + /** + * Get Data Store + * + * @return Zoninator_REST_Interfaces_Data_Store + */ + function get_data_store() { + return $this->data_store; + } + + /** + * Set the Data Store + * + * @param Zoninator_REST_Interfaces_Data_Store|Zoninator_REST_Data_Store_Builder $data_store A builder or a Data store. + * @return $this + * @throws Zoninator_REST_Exception Throws when Data Store Invalid. + */ + function set_data_store( $data_store ) { + if ( is_a( $data_store, 'Zoninator_REST_Data_Store_Builder' ) ) { + $this->data_store = $data_store + ->with_model_definition( $this ) + ->build(); + } else { + $this->data_store = $data_store; + } + // at this point we should have a data store. + Zoninator_REST_Expect::is_a( $this->data_store, 'Zoninator_REST_Interfaces_Data_Store' ); + + return $this; + } + + /** + * Environment + * + * @return Zoninator_REST_Environment + */ + function environment() { + return $this->environment; + } + + /** + * Get this Definition's Field Declarations + * + * @param null|string $filter_by_type The type to filter with. + * + * @return array|null + */ + function get_field_declarations( $filter_by_type = null ) { + $model_declaration = $this->get_model_declaration()->set_definition( $this ); + + Zoninator_REST_Expect::is_a( $model_declaration, 'Zoninator_REST_Interfaces_Model_Declaration' ); + + if ( null === $this->field_declarations ) { + $fields = $model_declaration->declare_fields( $this->environment() ); + + $this->field_declarations = $this->initialize_field_map( $fields ); + } + + if ( null === $filter_by_type ) { + return $this->field_declarations; + } + + $filtered = array(); + + foreach ( $this->field_declarations as $field_declaration ) { + /** + * The field declaration. + * + * @var Zoninator_REST_Field_Declaration $field_declaration + */ + if ( $field_declaration->get_kind() === $filter_by_type ) { + $filtered[] = $field_declaration; + } + } + return $filtered; + } + + /** + * Create a new Model Instance + * + * @param array $data The data. + * + * @return Zoninator_REST_Model + * @throws Zoninator_REST_Exception Throws if data not an array. + */ + function create_instance( $data ) { + if ( is_array( $data ) ) { + return new Zoninator_REST_Model( $this, $data ); + } + throw new Zoninator_REST_Exception( 'does not understand entity' ); + } + + /** + * * Merge values from array with current values. + * Note: Values change in place. + * + * @param Zoninator_REST_Interfaces_Model $model The model. + * @param array $data The data (key-value assumed). + * @param bool $updating Is this an update?. + * + * @return Zoninator_REST_Interfaces_Model|WP_Error + * @throws Zoninator_REST_Exception Throws. + */ + function update_model_from_array( $model, $data, $updating = false ) { + $mapped_data = $this->map_data( $data, $updating ); + foreach ( $mapped_data as $name => $value ) { + $model->set( $name, $value ); + } + return $model->sanitize(); + } + + /** + * Get Model Declaration + * + * @return Zoninator_REST_Interfaces_Model_Declaration + */ + public function get_model_declaration() { + return $this->model_declaration; + } + + /** + * Creates a new Model From a Request + * + * @param array $data The request. + * @return Zoninator_REST_Model|WP_Error + */ + public function new_from_array( $data ) { + $field_data = $this->map_data( $data, false ); + return $this->create_instance( $field_data )->sanitize(); + } + + /** + * Get field DTO Mappings + * + * @return array + */ + function get_dto_field_mappings() { + $mappings = array(); + foreach ( $this->get_field_declarations() as $field_declaration ) { + /** + * Declaration + * + * @var Zoninator_REST_Field_Declaration $field_declaration + */ + if ( ! $field_declaration->supports_output_type( 'json' ) ) { + continue; + } + $mappings[ $field_declaration->get_data_transfer_name() ] = $field_declaration->get_name(); + } + return $mappings; + } + + /** + * Prepare the Model for Data Transfer + * + * @param Zoninator_REST_Interfaces_Model $model The model. + * + * @return array + */ + function model_to_dto( $model ) { + $result = array(); + foreach ( $this->get_dto_field_mappings() as $mapping_name => $field_name ) { + $value = $model->get( $field_name ); + $result[ $mapping_name ] = $value; + } + + return $result; + } + + /** + * Get Name + * + * @return string + */ + public function get_name() { + return $this->name; + } + + /** + * Check permissions + * + * @param WP_REST_Request $request The request. + * @param string $action The action. + * @return bool + */ + public function permissions_check( $request, $action ) { + return $this->permissions_provider->permissions_check( $request, $action ); + } + + /** + * Map data names + * + * @param array $data The data to map. + * @param bool $updating Are we Updating. + * @return array + */ + private function map_data( $data, $updating = false ) { + $request_data = array(); + $fields = $this->get_field_declarations(); + foreach ( $fields as $field ) { + /** + * Field + * + * @var Zoninator_REST_Field_Declaration $field Field. + */ + if ( $field->is_kind( Zoninator_REST_Field_Declaration::DERIVED ) ) { + continue; + } + $dto_name = $field->get_data_transfer_name(); + $field_name = $field->get_name(); + if ( isset( $data[ $dto_name ] ) && ! ( $updating && $field->is_primary() ) ) { + $value = $data[ $dto_name ]; + $request_data[ $field_name ] = $value; + } + } + return $request_data; + } + + /** + * Initialize_field_map + * + * @param array $declared_field_builders Array. + * + * @return array + */ + private function initialize_field_map( $declared_field_builders ) { + $fields = array(); + foreach ( $declared_field_builders as $field_builder ) { + /** + * Builder + * + * @var Zoninator_REST_Field_Declaration $field Field Builder. + */ + $field = $field_builder->build(); + $fields[ $field->get_name() ] = $field; + } + return $fields; + } +} diff --git a/lib/zoninator_rest/model/class-zoninator-rest-model-settings.php b/lib/zoninator_rest/model/class-zoninator-rest-model-settings.php new file mode 100644 index 0000000..cff5594 --- /dev/null +++ b/lib/zoninator_rest/model/class-zoninator-rest-model-settings.php @@ -0,0 +1,163 @@ +get_environment(); + $settings_per_group = $this->get_settings(); + $fields = array(); + + foreach ( $settings_per_group as $group_name => $group_data ) { + $group_fields = $group_data[1]; + + foreach ( $group_fields as $field_data ) { + $field_builder = $this->field_declaration_builder_from_data( $env, $field_data ); + $fields[] = $field_builder; + } + } + return $fields; + } + + /** + * Convert bool to bit + * + * @param mixed $value Val. + * @return string + */ + public function bool_to_bit( $value ) { + return ( ! empty( $value ) && 'false' !== $value ) ? '1' : ''; + } + + /** + * Covert bit to bool + * + * @param mixed $value Val. + * @return bool + */ + public function bit_to_bool( $value ) { + return ( ! empty( $value ) && '0' !== $value ) ? true : false; + } + + /** + * Get ID + * + * @return string + */ + public function get_id() { + return strtolower( get_class( $this ) ); + } + + /** + * Set ID + * + * @param mixed $new_id New ID. + * @return Zoninator_REST_Interfaces_Model $this + */ + public function set_id( $new_id ) { + return $this; + } + + /** + * Build declarations from array + * + * @param Zoninator_REST_Environment $env Environment. + * @param array $field_data Data. + * @return Zoninator_REST_Field_Declaration_Builder + */ + private function field_declaration_builder_from_data( $env, $field_data ) { + $field_name = $field_data['name']; + $field_builder = $env->field( $field_name ); + $default_value = isset( $field_data['std'] ) ? $field_data['std'] : $this->default_for_attribute( $field_data, 'std' ); + $label = isset( $field_data['label'] ) ? $field_data['label'] : $field_name; + $description = isset( $field_data['desc'] ) ? $field_data['desc'] : $label; + $setting_type = isset( $field_data['type'] ) ? $field_data['type'] : null; + $choices = isset( $field_data['options'] ) ? array_keys( $field_data['options'] ) : null; + $field_type = 'string'; + + if ( 'checkbox' === $setting_type ) { + $field_type = 'boolean'; + if ( $default_value ) { + // convert our default value as well. + $default_value = $this->bit_to_bool( $default_value ); + } + $field_builder + ->with_serializer( array( $this, 'bool_to_bit' ) ) + ->with_deserializer( array( $this, 'bit_to_bool' ) ); + + } elseif ( 'select' === $setting_type ) { + $field_type = 'string'; + } else { + // try to guess numeric fields, although this is not perfect. + if ( is_numeric( $default_value ) ) { + $field_type = is_float( $default_value ) ? 'float' : 'integer'; + } + } + + if ( $default_value ) { + $field_builder->with_default( $default_value ); + } + $field_builder + ->with_description( $description ) + ->with_dto_name( $field_name ) + ->with_type( $env->type( $field_type ) ); + if ( $choices ) { + $field_builder->with_choices( $choices ); + } + + $this->on_field_setup( $field_name, $field_builder, $field_data, $env ); + + return $field_builder; + } +} diff --git a/lib/zoninator_rest/model/class-zoninator-rest-model-validationdata.php b/lib/zoninator_rest/model/class-zoninator-rest-model-validationdata.php new file mode 100644 index 0000000..eec9b06 --- /dev/null +++ b/lib/zoninator_rest/model/class-zoninator-rest-model-validationdata.php @@ -0,0 +1,75 @@ +value = $value; + $this->model = $model; + $this->field = $field; + } + + + /** + * Get Value + * + * @return mixed $this->value the value that needs validation + */ + public function get_value() { + return $this->value; + } + + /** + * Get Model + * + * @return Zoninator_REST_Interfaces_Model + */ + public function get_model() { + return $this->model; + } + + /** + * Get Field + * + * @return Zoninator_REST_Field_Declaration + */ + public function get_field() { + return $this->field; + } +} diff --git a/lib/zoninator_rest/model/declaration/class-zoninator-rest-model-declaration-settings.php b/lib/zoninator_rest/model/declaration/class-zoninator-rest-model-declaration-settings.php new file mode 100644 index 0000000..4ce3e2d --- /dev/null +++ b/lib/zoninator_rest/model/declaration/class-zoninator-rest-model-declaration-settings.php @@ -0,0 +1,174 @@ +get_settings(); + $fields = array(); + + foreach ( $settings_per_group as $group_name => $group_data ) { + $group_fields = $group_data[1]; + + foreach ( $group_fields as $field_data ) { + $field_builder = $this->field_declaration_builder_from_data( $env, $field_data ); + $fields[] = $field_builder; + } + } + return $fields; + } + + /** + * Convert bool to bit + * + * @param mixed $value Val. + * @return string + */ + function bool_to_bit( $value ) { + return ( ! empty( $value ) && 'false' !== $value ) ? '1' : ''; + } + + /** + * Covert bit to bool + * + * @param mixed $value Val. + * @return bool + */ + function bit_to_bool( $value ) { + return ( ! empty( $value ) && '0' !== $value ) ? true : false; + } + + /** + * Get ID + * + * @param Zoninator_REST_Interfaces_Model $model Model. + * @return string + */ + function get_id( $model ) { + return strtolower( get_class( $this ) ); + } + + /** + * Set ID + * + * @param Zoninator_REST_Interfaces_Model $model Model. + * @param mixed $new_id New ID. + * @return Zoninator_REST_Interfaces_Model $this + */ + function set_id( $model, $new_id ) { + return $this; + } + + /** + * Build declarations from array + * + * @param Zoninator_REST_Environment $env Environment. + * @param array $field_data Data. + * @return Zoninator_REST_Field_Declaration_Builder + */ + private function field_declaration_builder_from_data( $env, $field_data ) { + $field_name = $field_data['name']; + $field_builder = $env->field( $field_name ); + $default_value = isset( $field_data['std'] ) ? $field_data['std'] : $this->default_for_attribute( $field_data, 'std' ); + $label = isset( $field_data['label'] ) ? $field_data['label'] : $field_name; + $description = isset( $field_data['desc'] ) ? $field_data['desc'] : $label; + $setting_type = isset( $field_data['type'] ) ? $field_data['type'] : null; + $choices = isset( $field_data['options'] ) ? array_keys( $field_data['options'] ) : null; + $field_type = 'string'; + + if ( 'checkbox' === $setting_type ) { + $field_type = 'boolean'; + if ( $default_value ) { + // convert our default value as well. + $default_value = $this->bit_to_bool( $default_value ); + } + $field_builder + ->with_serializer( array( $this, 'bool_to_bit' ) ) + ->with_deserializer( array( $this, 'bit_to_bool' ) ); + + } elseif ( 'select' === $setting_type ) { + $field_type = 'string'; + } else { + // try to guess numeric fields, although this is not perfect. + if ( is_numeric( $default_value ) ) { + $field_type = is_float( $default_value ) ? 'float' : 'integer'; + } + } + + if ( $default_value ) { + $field_builder->with_default( $default_value ); + } + $field_builder + ->with_description( $description ) + ->with_dto_name( $field_name ) + ->with_type( $env->type( $field_type ) ); + if ( $choices ) { + $field_builder->with_choices( $choices ); + } + + $this->on_field_setup( $field_name, $field_builder, $field_data, $env ); + return $field_builder; + } + + /** + * Permissions Check + * + * @param WP_REST_Request $request Request. + * @param string $action Action. + * @return bool + */ + public function permissions_check( $request, $action ) { + return true; + } +} diff --git a/lib/zoninator_rest/model/definition/class-zoninator-rest-model-definition-builder.php b/lib/zoninator_rest/model/definition/class-zoninator-rest-model-definition-builder.php new file mode 100644 index 0000000..fda5116 --- /dev/null +++ b/lib/zoninator_rest/model/definition/class-zoninator-rest-model-definition-builder.php @@ -0,0 +1,108 @@ +with_data_store( new Zoninator_REST_Data_Store_Nil() ) + ->with_permissions_provider( new Zoninator_REST_Permissions_Any() ); + } + + /** + * With Declaration + * + * @param Zoninator_REST_Interfaces_Model_Declaration|Zoninator_REST_Interfaces_Permissions_Provider $declaration D. + * @return Zoninator_REST_Model_Definition_Builder + */ + function with_declaration( $declaration ) { + if ( is_string( $declaration ) && class_exists( $declaration ) ) { + $declaration = new $declaration(); + } + Zoninator_REST_Expect::is_a( $declaration, 'Zoninator_REST_Interfaces_Model_Declaration' ); + $this->declaration = $declaration; + if ( is_a( $declaration, 'Zoninator_REST_Interfaces_Permissions_Provider' ) ) { + $this->with_permissions_provider( $declaration ); + } + return $this; + } + + /** + * With Data Store + * + * @param null|Zoninator_REST_Interfaces_Builder $data_store Data Store. + * + * @return Zoninator_REST_Model_Definition_Builder $this + */ + function with_data_store( $data_store = null ) { + $this->data_store = $data_store; + return $this; + } + + /** + * With Permissions Provider + * + * @param Zoninator_REST_Interfaces_Permissions_Provider $permissions_provider Provider. + */ + function with_permissions_provider( $permissions_provider ) { + $this->permissions_provider = $permissions_provider; + } + + /** + * With Environment + * + * @param Zoninator_REST_Environment $environment Environment. + * + * @return Zoninator_REST_Model_Definition_Builder $this + */ + function with_environment( $environment ) { + $this->environment = $environment; + return $this; + } + + /** + * Build + * + * @return Zoninator_REST_Model_Definition + */ + function build() { + return new Zoninator_REST_Model_Definition( $this->environment, $this->declaration, $this->data_store, $this->permissions_provider ); + } +} diff --git a/lib/zoninator_rest/permissions/class-zoninator-rest-permissions-any.php b/lib/zoninator_rest/permissions/class-zoninator-rest-permissions-any.php new file mode 100644 index 0000000..6e10edb --- /dev/null +++ b/lib/zoninator_rest/permissions/class-zoninator-rest-permissions-any.php @@ -0,0 +1,27 @@ +unsigned = $unsigned; + parent::__construct( 'integer' ); + } + + /** + * Default + * + * @return int + */ + public function default_value() { + return 0; + } + + /** + * Cast + * + * @param mixed $value Val. + * @return int + */ + public function cast( $value ) { + if ( ! is_numeric( $value ) ) { + return $this->default_value(); + } + return $this->unsigned ? absint( $value ) : intval( $value, 10 ); + } + + /** + * Sanitize + * + * @param mixed $value Val. + * @return int + */ + function sanitize( $value ) { + return $this->cast( $value ); + } +} diff --git a/lib/zoninator_rest/type/class-zoninator-rest-type-nullable.php b/lib/zoninator_rest/type/class-zoninator-rest-type-nullable.php new file mode 100644 index 0000000..3712378 --- /dev/null +++ b/lib/zoninator_rest/type/class-zoninator-rest-type-nullable.php @@ -0,0 +1,75 @@ +name() ); + $this->item_type_definition = $item_type_definition; + } + + /** + * Default value as always null. + * + * @return null + */ + public function default_value() { + return null; + } + + /** + * Cast + * + * @param mixed $value Value. + * @return mixed|null + */ + public function cast( $value ) { + if ( null === $value ) { + return null; + } + return $this->item_type_definition->cast( $value ); + } + + /** + * Sanitize. + * + * @param mixed $value Value. + * @return mixed|null + */ + public function sanitize( $value ) { + if ( null === $value ) { + return null; + } + return $this->item_type_definition->sanitize( $value ); + } + + /** + * Schema + */ + function schema() { + $schema = parent::schema(); + $schema['type'] = array_unique( array_merge( $schema['type'], array( 'null' ) ) ); + } +} diff --git a/lib/zoninator_rest/type/class-zoninator-rest-type-number.php b/lib/zoninator_rest/type/class-zoninator-rest-type-number.php new file mode 100644 index 0000000..0cdb773 --- /dev/null +++ b/lib/zoninator_rest/type/class-zoninator-rest-type-number.php @@ -0,0 +1,55 @@ +default_value(); + } + return floatval( $value ); + } + + /** + * Sanitize + * + * @param mixed $value The value to sanitize. + * @return float + */ + function sanitize( $value ) { + return $this->cast( $value ); + } +} diff --git a/lib/zoninator_rest/type/class-zoninator-rest-type-registry.php b/lib/zoninator_rest/type/class-zoninator-rest-type-registry.php new file mode 100644 index 0000000..12cdfca --- /dev/null +++ b/lib/zoninator_rest/type/class-zoninator-rest-type-registry.php @@ -0,0 +1,125 @@ +types[ $identifier ] = $instance; + return $this; + } + + /** + * Get a type definition + * + * @param string $type The type name. + * @return Zoninator_REST_Interfaces_Type + * + * @throws Zoninator_REST_Exception In case of type name not confirming to syntax. + */ + function definition( $type ) { + $types = $this->get_types(); + + if ( ! isset( $types[ $type ] ) ) { + // maybe lazy-register missing compound type. + $parts = explode( ':', $type ); + if ( count( $parts ) > 1 ) { + + $container_type = $parts[0]; + if ( ! in_array( $container_type, $this->container_types, true ) ) { + throw new Zoninator_REST_Exception( $container_type . ' is not a known container type' ); + } + + $item_type = $parts[1]; + if ( empty( $item_type ) ) { + throw new Zoninator_REST_Exception( $type . ': invalid syntax' ); + } + $item_type_definition = $this->definition( $item_type ); + + if ( 'array' === $container_type ) { + $this->define( $type, new Zoninator_REST_Type_TypedArray( $item_type_definition ) ); + $types = $this->get_types(); + } + + if ( 'nullable' === $container_type ) { + $this->define( $type, new Zoninator_REST_Type_Nullable( $item_type_definition ) ); + $types = $this->get_types(); + } + } + } + + if ( ! isset( $types[ $type ] ) ) { + throw new Zoninator_REST_Exception(); + } + return $types[ $type ]; + } + + /** + * Get Types + * + * @return array + */ + private function get_types() { + return (array) apply_filters( 'mixtape_type_registry_get_types', $this->types, $this ); + } + + /** + * Initialize the type registry + * + * @param Zoninator_REST_Environment $environment The Environment. + */ + public function initialize( $environment ) { + if ( null !== $this->types ) { + return; + } + + $this->types = apply_filters( 'mixtape_type_registry_register_types', array( + 'any' => new Zoninator_REST_Type( 'any' ), + 'string' => new Zoninator_REST_Type_String(), + 'integer' => new Zoninator_REST_Type_Integer(), + 'int' => new Zoninator_REST_Type_Integer(), + 'uint' => new Zoninator_REST_Type_Integer( true ), + 'number' => new Zoninator_REST_Type_Number(), + 'float' => new Zoninator_REST_Type_Number(), + 'boolean' => new Zoninator_REST_Type_Boolean(), + 'array' => new Zoninator_REST_Type_Array(), + ), $this, $environment ); + } +} diff --git a/lib/zoninator_rest/type/class-zoninator-rest-type-string.php b/lib/zoninator_rest/type/class-zoninator-rest-type-string.php new file mode 100644 index 0000000..40ba8ac --- /dev/null +++ b/lib/zoninator_rest/type/class-zoninator-rest-type-string.php @@ -0,0 +1,58 @@ +cast( $v ); + } + return '(' . implode( ',', $cast_ones ) . ')'; + } + return (string) $value; + } +} diff --git a/lib/zoninator_rest/type/class-zoninator-rest-type-typedarray.php b/lib/zoninator_rest/type/class-zoninator-rest-type-typedarray.php new file mode 100644 index 0000000..34b84ab --- /dev/null +++ b/lib/zoninator_rest/type/class-zoninator-rest-type-typedarray.php @@ -0,0 +1,71 @@ +name() ); + $this->item_type_definition = $item_type_definition; + } + + /** + * Get the default value + * + * @return array + */ + public function default_value() { + return array(); + } + + /** + * Cast the value to be a typed array + * + * @param mixed $value an array of mixed. + * @return array + */ + public function cast( $value ) { + $new_value = array(); + + foreach ( $value as $v ) { + $new_value[] = $this->item_type_definition->cast( $v ); + } + return (array) $new_value; + } + + /** + * Get this type's JSON Schema + * + * @return array + */ + function schema() { + $schema = parent::schema(); + $schema['type'] = 'array'; + $schema['items'] = $this->item_type_definition->schema(); + return $schema; + } +}