diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a880a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +bin +vendor +composer.lock +*.sublime-project +*.sublime-workspace +nbproject/ diff --git a/.travis.yml b/.travis.yml index d494c8c..215bc4a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,11 +4,15 @@ php: - 5.3 - 5.4 - 5.5 + - 5.6 + - hhvm env: - - WP_VERSION=latest WP_MULTISITE=0 + - WP_VERSION=3.8 + - WP_VERSION=latest before_script: - - bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION + - bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION + - fgrep wp_version /tmp/wordpress/wp-includes/version.php script: phpunit diff --git a/behat.yml b/behat.yml new file mode 100644 index 0000000..6b10c9c --- /dev/null +++ b/behat.yml @@ -0,0 +1,16 @@ +default: + context: + parameters: + base_url: http://localhost:8080/ + role_map: + ender: subscriber + starter: editor + extensions: + Behat\MinkExtension\Extension: + base_url: http://localhost:8080/ + goutte: ~ + selenium2: + wd_host: http://localhost:8910/wd/hub + browser_name: phantomjs + default_session: selenium2 + javascript_session: selenium2 \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..65d629a --- /dev/null +++ b/composer.json @@ -0,0 +1,16 @@ +{ + "minimum-stability": "stable", + "config": { + "bin-dir": "bin", + "vendor-dir": "vendor" + }, + "require-dev": { + "phpunit/phpunit": "4.1.*", + "behat/behat": "2.5.3", + "behat/behat": "2.5.3", + "behat/mink": "1.5", + "behat/mink-extension": "*", + "behat/mink-goutte-driver": "*", + "behat/mink-selenium2-driver": "*" + } +} diff --git a/features/bootstrap/FeatureContext.php b/features/bootstrap/FeatureContext.php new file mode 100644 index 0000000..d615d33 --- /dev/null +++ b/features/bootstrap/FeatureContext.php @@ -0,0 +1,67 @@ +login($username, $password); + } + + /** + * @When /^I write a post with title "([^"]*)" and content "([^"]*)"$/ + */ + public function iWriteAPostWithTitleAndContent($post_title, $content) + { + $this->fill_in_post('post', $post_title, 'publish', $content); + } + + /** + * @Given /^the plugin "([^"]*)" is "([^"]*)"$/ + */ + public function thePluginIs($plugin, $state) + { + if ($state == "active") { + $action = "activate"; + } else { + $action = "deactivate"; + } + shell_exec(escapeshellcmd("wp plugin $action $plugin")); + } + + /** + * @When /^I search for "([^"]*)"$/ + */ + public function iSearchFor($term) + { + return array( + new When("I fill in \"s\" with \"$term\""), + new When("I press \"searchsubmit\""), + ); + } + +} diff --git a/features/bootstrap/WordPressContext.php b/features/bootstrap/WordPressContext.php new file mode 100644 index 0000000..130cecc --- /dev/null +++ b/features/bootstrap/WordPressContext.php @@ -0,0 +1,248 @@ +base_url = $params['base_url']; + $this->role_map = $params['role_map']; + } + + /** + * Given a list of usernames (user_login field), checks for every username + * if they exist. Returns a list of the users that do not exist. + * + * @param array $users + * @return array + * @author Maarten Jacobs + **/ + protected function check_users_exist(array $users) { + $session = $this->getSession(); + + // Check if the users exist, saving the inexistent users + $inexistent_users = array(); + $this->visit( 'wp-admin/users.php' ); + $current_page = $session->getPage(); + foreach ($users as $username) { + if (!$current_page->hasContent($username)) { + $inexistent_users[] = $username; + } + } + + return $inexistent_users; + } + + /** + * Creates a user for every username given (user_login field). + * The inner values can also maps of the following type: + * array( + * 'username' => + * 'password' => (default: pass) + * 'email' => (default: username@test.dev) + * 'role' => (default: checks rolemap, or 'subscriber') + * ) + * + * @param array $users + * @author Maarten Jacobs + **/ + protected function create_users(array $users) { + $session = $this->getSession(); + + foreach ($users as $username) { + if (is_array($username)) { + $name = $username['username']; + $password = array_key_exists('password', $username) ? $username['password'] : 'pass'; + $email = array_key_exists('email', $username) ? $username['email'] : str_replace(' ', '_', $name) . '@test.dev'; + } else { + $name = $username; + $password = 'pass'; + $email = str_replace(' ', '_', $name) . '@test.dev'; + } + + $this->visit( 'wp-admin/user-new.php' ); + $current_page = $session->getPage(); + + // Fill in the form + $current_page->findField('user_login')->setValue($name); + $current_page->findField('email')->setValue($email); + $current_page->findField('pass1')->setValue($password); + $current_page->findField('pass2')->setValue($password); + + // Set role + $role = ucfirst( strtolower( $this->role_map[$name] ) ); + $current_page->findField('role')->selectOption($role); + + // Submit form + $current_page->findButton('Add New User')->click(); + } + } + + /** + * Fills in the form of a generic post. + * Given the status, will either publish or save as draft. + * + * @param string $post_type + * @param string $post_title + * @param string $status Either 'draft' or anything else for 'publish' + * @author Maarten Jacobs + **/ + protected function fill_in_post($post_type, $post_title, $status = 'publish', $content = '

Testing all the things. All the time.

') { + // The post type, if not post, will be appended. + // Rather than a separate page per type, this is how WP works with forms for separate post types. + $uri_suffix = $post_type !== 'post' ? '?post_type=' . $post_type : ''; + $this->visit( 'wp-admin/post-new.php' . $uri_suffix ); + $session = $this->session = $this->getSession(); + $current_page = $session->getPage(); + + // Fill in the title + $current_page->findField( 'post_title' )->setValue( $post_title ); + // Fill in some nonsencical data for the body + // clickLink and setValue seem to be failing for TinyMCE (same for Cucumber unfortunately) + $session->executeScript( 'jQuery( "#content-html" ).click()' ); + $session->executeScript( 'jQuery( "#content" ).val( "' . $content . '" )' ); + + // Click the appropriate button depending on the given status + $state_button = 'Save Draft'; + switch ($status) { + case 'draft': + // We're good. + break; + + case 'publish': + default: + // Save as draft first + $current_page->findButton($state_button)->click(); + $state_button = 'Publish'; + break; + } + $current_page->findButton($state_button)->click(); + // go to view of new post + $session->getPage()->clickLink("View $post_type"); + } + + /** + * Makes sure the current user is logged out, and then logs in with + * the given username and password. + * + * @param string $username + * @param string $password + * @author Maarten Jacobs + **/ + protected function login($username, $password = 'pass') { + $session = $this->session = $this->getSession(); + $current_page = $session->getPage(); + + // Check if logged in as that user + $this->visit( 'wp-admin' ); + if ($current_page->hasContent( "Howdy, {$username}" )) { + // We're already logged in as this user. + // Double-check + $this->assertPageContainsText('Dashboard'); + return true; + } + + // Logout + $this->visit( 'wp-login.php?action=logout' ); + if ($session->getPage()->hasLink('log out')) { + $session->getPage()->clickLink('log out'); + $current_page = $session->getPage(); + } + + // And login + $current_page->fillField('user_login', $username); + $current_page->fillField('user_pass', $password); + $current_page->findButton('wp-submit')->click(); + + // Assert that we are on the dashboard + $this->assertPageContainsText('Dashboard'); + } + + /** + * Given the current page is post list page, we enter the title in the searchbox + * and search for that post. + * + * @param string $post_title The title of the post as it would appear in the WP backend + * @param boolean $do_assert If set to anything but false, will assert for the existence of the post title after the search + * @return void + * @author Maarten Jacobs + **/ + protected function searchForPost( $post_title, $do_assert = FALSE ) { + + $current_page = $this->getSession()->getPage(); + + // Search for the post + $search_field = $current_page->findField( 'post-search-input' ); // Searching on #id + // When there is no content, then the searchbox is not shown + // So we skip search in that case + if ($search_field) { + $search_field->setValue( $post_title ); + + $current_page->findField( 'Search Posts' ) // Searching on value + ->click(); + } + + // We don't stop tests even if the searchbox does not exist. + // That would prevent the dev from knowing what the hell's going on. + // Can I assert all the things? + if ( $do_assert ) { + $this->assertPageContainsText($post_title); + } + + } + + /** + * @Given /^I trash the "([^"]*)" titled "([^"]*)"$/ + */ + public function iTrashThePostTitled( $post_type, $post_title ) { + + $session = $this->session = $this->getSession(); + + // Visit the posts page + $uri_suffix = $post_type !== 'post' ? '?post_type=' . $post_type : ''; + $postlist_uri = 'wp-admin/edit.php' . $uri_suffix; + $this->visit( $postlist_uri ); + $current_page = $session->getPage(); + + // Check if the post with that title is on the current page + if (!$current_page->hasContent( $post_title )) { + // If not, search for the post + $this->searchForPost( $post_title ); + } + $this->assertPageContainsText($post_title); + + // Select the post in the checkbox column + // This is tricky: the checkbox has a non-unique name (of course, that's the way to do it) + // So we need to check the box in a different way + // The easiest: jQuery + $session->executeScript( "jQuery( \"tr:contains('$post_title') :checkbox\" ).click()" ); + + // Trash it + // - Select the 'Move to Trash' option + $current_page->selectFieldOption( 'action', 'Move to Trash' ); + // - Click to Apply + $current_page->findButton( 'doaction' )->click(); + + // Check if the post is no longer visible on the posts page + $this->visit( $postlist_uri ); + $this->assertPageNotContainsText( $post_title ); + // Make a search, because it could be on another page + $this->searchForPost( $post_title ); + $this->assertPageNotContainsText( $post_title ); + + } + +} diff --git a/features/shortcodes.feature b/features/shortcodes.feature new file mode 100644 index 0000000..28df0fd --- /dev/null +++ b/features/shortcodes.feature @@ -0,0 +1,21 @@ +Feature: Use Shortcodes + In order to use my Plugin + As a website author + I need to write posts with shortcodes + + Background: + Given I am logged in as "admin" with "vagrant" + + Scenario: Without the plugin + Given the plugin "freifunkmeta" is "inactive" + When I write a post with title "test" and content "[ff_contact]" + #Then print current URL + Then I should see "ff_contact" + + Scenario: With the plugin + Given the plugin "freifunkmeta" is "active" + When I write a post with title "test" and content "[ff_contact]" + #Then print current URL + Then I should see "Twitter" in the ".ff_contact" element + And I should not see "ff_contact" + diff --git a/features/wp-search.feature b/features/wp-search.feature new file mode 100644 index 0000000..879e7d8 --- /dev/null +++ b/features/wp-search.feature @@ -0,0 +1,10 @@ +Feature: Search + In order to find older articles + As a website user + I need to be able to search for a word + + Scenario: Searching for a post + Given I am on the homepage + When I search for "Welcome" + Then I should see "Hello world!" + And I should see "Welcome to WordPress. This is your first post." diff --git a/freifunkmeta.php b/freifunkmeta.php index 4eac73a..8818cb7 100644 --- a/freifunkmeta.php +++ b/freifunkmeta.php @@ -3,34 +3,49 @@ Plugin Name: Freifunk Metadata Shortcodes Plugin URI: http://mschuette.name/ Description: Defines shortcodes to display Freifunk metadata -Version: 0.3 +Version: 0.4dev Author: Martin Schuette Author URI: http://mschuette.name/ */ -define('FF_META_DEFAULT_CACHETIME', 15); -define('FF_META_DEFAULT_DIR', 'https://raw.githubusercontent.com/freifunk/directory.api.freifunk.net/master/directory.json'); -define('FF_META_DEFAULT_CITY', 'hamburg'); +define( 'FF_META_DEFAULT_CACHETIME', 15 ); +define( 'FF_META_DEFAULT_DIR', 'https://raw.githubusercontent.com/freifunk/directory.api.freifunk.net/master/directory.json' ); +define( 'FF_META_DEFAULT_CITY', 'hamburg' ); /** * class to fetch and cache data from external URLs + * returns either an array from decoded JSON data, or WP_Error */ class FF_Meta_Externaldata { - public function get($url) { - /* gets metadata from URL, handles caching */ - $cachekey = 'ff_metadata_'.hash('crc32', $url); - $cachetime = get_option('ff_meta_cachetime', FF_META_DEFAULT_CACHETIME) * MINUTE_IN_SECONDS; + public function get( $url ) { + //error_log( "FF_Meta_Externaldata::get( $url )" ); + /* gets metadata from URL, handles caching, + * hashed because cache keys should be <= 40 chars */ + $cachekey = 'ff_metadata_'.hash( 'crc32', $url ); + $cachetime = get_option( + 'ff_meta_cachetime', FF_META_DEFAULT_CACHETIME + ) * MINUTE_IN_SECONDS; - // Caching - if ( false === ( $data = get_transient($cachekey) ) ) { - $http_response = wp_remote_get($url); - $json = wp_remote_retrieve_body($http_response); - $data = json_decode ($json, $assoc = true); - set_transient( $cachekey, $data, $cachetime ); - } - return $data; - } + // Caching + if ( WP_DEBUG || ( false === ( $data = get_transient( $cachekey ) ) ) ) { + $args = array( 'sslverify' => false ); + $http_response = wp_remote_get( $url, $args ); + if ( is_wp_error( $http_response ) ) { + $error_msg = sprintf( + 'Unable to retrieve URL %s, error: %s', + $url, $http_response->get_error_message() + ); + error_log( $error_msg, 4 ); + return $http_response; + } else { + $json = wp_remote_retrieve_body( $http_response ); + $data = json_decode( $json, $assoc = true ); + set_transient( $cachekey, $data, $cachetime ); + } + } + return $data; + } } /** @@ -38,22 +53,42 @@ class FF_Meta_Externaldata */ class FF_Directory { - private $directory; + private $directory; + private $ed; - function __construct() { - $ed = new FF_Meta_Externaldata(); - $this->directory = $ed->get(FF_META_DEFAULT_DIR); - } + function __construct( $ext_data_service = null ) { + if ( is_null( $ext_data_service ) ) { + $this->ed = new FF_Meta_Externaldata(); + } else { + $this->ed = $ext_data_service; + } + $data = $this->ed->get( FF_META_DEFAULT_DIR ); + if ( is_wp_error( $data ) ) { + $this->directory = array(); + } else { + $this->directory = $data; + } + } - function get_url_by_city($city) { - $val = $this->directory[$city]; + function get_url_by_city( $city ) { + if ( array_key_exists( $city, $this->directory ) ) { + return $this->directory[$city]; + } else { + return false; + } + } - if (empty($val)) { - return false; - } else { - return $val; - } - } + // get one big array of all known community data + function get_all_data() { + $all_locs = array(); + foreach ( $this->directory as $tmp_city => $url ) { + $tmp_meta = $this->ed->get( $url ); + if ( ! is_wp_error( $tmp_meta ) ) { + $all_locs[$tmp_city] = $tmp_meta; + } + } + return $all_locs; + } } /** @@ -61,49 +96,63 @@ class FF_Directory */ class FF_Community { - public $name; - public $street; - public $zip; - public $city; - public $lon; - public $lat; + public $name; + public $street; + public $zip; + public $city; + public $lon; + public $lat; - /** - * Default constructor from metadata - */ - function __construct($metadata) { - $loc = $metadata['location']; - $this->name = (isset($loc['address']) && isset($loc['address']['Name'])) ? $loc['address']['Name'] : ''; - $this->street = (isset($loc['address']) && isset($loc['address']['Street'])) ? $loc['address']['Street'] : ''; - $this->zip = (isset($loc['address']) && isset($loc['address']['Zipcode'])) ? $loc['address']['Zipcode'] : ''; - $this->city = isset($loc['city']) ? $loc['city'] : ''; - $this->lon = isset($loc['lon']) ? $loc['lon'] : ''; - $this->lat = isset($loc['lat']) ? $loc['lat'] : ''; - } + /** + * Default constructor from metadata + */ + function __construct( $metadata ) { + $loc = $metadata['location']; + $this->name = ( isset( $loc['address'] ) && isset( $loc['address']['Name'] ) ) + ? $loc['address']['Name'] : ''; + $this->street = ( isset( $loc['address'] ) && isset( $loc['address']['Street'] ) ) + ? $loc['address']['Street'] : ''; + $this->zip = ( isset( $loc['address'] ) && isset( $loc['address']['Zipcode'] ) ) + ? $loc['address']['Zipcode'] : ''; + $this->city = isset( $loc['city'] ) ? $loc['city'] : ''; + $this->lon = isset( $loc['lon'] ) ? $loc['lon'] : ''; + $this->lat = isset( $loc['lat'] ) ? $loc['lat'] : ''; + } - /** - * Alternative constructor from city name - */ - static function make_from_city($city) { - // TODO: test - if (false === ($url = $this->dir->get_url_by_city($city))) { - return "\n"; - } - if (false === ($metadata = FF_Meta_Externaldata::get($url))) { - return "\n"; - } - return new FF_Community($metadata); - } + /** + * Alternative constructor from city name + */ + static function make_from_city( $city, $ext_data_service = null ) { + if ( is_null( $ext_data_service ) ) { + $ed = new FF_Meta_Externaldata(); + } else { + $ed = $ext_data_service; + } + $directory = new FF_Directory( $ed ); - function format_address() { - if (empty($this->name) || empty($this->street) || empty($this->zip)) { - return ''; - } - // TODO: style address + map as single box - // TODO: once it is "ready" package openlayers.js into the plugin (cf. http://docs.openlayers.org/library/deploying.html) - // TODO: handle missing values (i.e. only name & city) - return '

' . sprintf('%s
%s
%s %s', $this->name, $this->street, $this->zip, $this->city) . '

'; - } + if ( false === ( $url = $directory->get_url_by_city( $city ) ) ) { + return '\n"; + } + if ( false === ( $metadata = $ed->get( $url ) ) ) { + return "\n"; + } + return new FF_Community( $metadata ); + } + + function format_address() { + if ( empty( $this->name ) || empty( $this->street ) || empty( $this->zip ) ) { + return ''; + } + // TODO: style address + map as single box + // TODO: once it is "ready" package openlayers.js into the plugin + // ( cf. http://docs.openlayers.org/library/deploying.html ) + // TODO: handle missing values ( i.e. only name & city ) + return sprintf( + '

%s
%s
%s %s

', + $this->name, $this->street, $this->zip, $this->city + ); + } } /** @@ -111,298 +160,374 @@ class FF_Community */ class FF_Meta { - private $dir; + private $dir; + private $ed; - function __construct() { - $this->dir = new FF_Directory(); - } + function reinit_external_data_service( $ext_data_service = null ) { + if ( is_null( $ext_data_service ) ) { + $this->ed = new FF_Meta_Externaldata(); + } else { + $this->ed = $ext_data_service; + } + $this->dir = new FF_Directory( $this->ed ); + } - function register_stuff() { - if ( ! shortcode_exists('ff_state') ) { - add_shortcode('ff_state', array($this, 'shortcode_handler')); - } - if ( ! shortcode_exists('ff_services') ) { - add_shortcode('ff_services', array($this, 'shortcode_handler')); - } - if ( ! shortcode_exists('ff_contact') ) { - add_shortcode('ff_contact', array($this, 'shortcode_handler')); - } - if ( ! shortcode_exists('ff_location') ) { - add_shortcode('ff_location', array($this, 'shortcode_handler')); - } + function __construct( $ext_data_service = null ) { + if ( is_null( $ext_data_service ) ) { + $this->ed = new FF_Meta_Externaldata(); + } else { + $this->ed = $ext_data_service; + } + $this->dir = new FF_Directory( $this->ed ); + } - add_action('admin_menu', array($this, 'admin_menu')); - add_action('admin_init', array($this, 'admin_init')); - register_uninstall_hook( __FILE__, array('ff_meta', 'uninstall_hook')); - } + function register_stuff() { + if ( ! shortcode_exists( 'ff_state' ) ) { + add_shortcode( 'ff_state', array( $this, 'shortcode_handler' ) ); + } + if ( ! shortcode_exists( 'ff_services' ) ) { + add_shortcode( 'ff_services', array( $this, 'shortcode_handler' ) ); + } + if ( ! shortcode_exists( 'ff_contact' ) ) { + add_shortcode( 'ff_contact', array( $this, 'shortcode_handler' ) ); + } + if ( ! shortcode_exists( 'ff_location' ) ) { + add_shortcode( 'ff_location', array( $this, 'shortcode_handler' ) ); + } + if ( ! shortcode_exists( 'ff_list' ) ) { + add_shortcode( 'ff_list', array( $this, 'shortcode_handler' ) ); + } - function output_ff_state($citydata) { - $state = $citydata['state']; - return sprintf('%s', $state['nodes']); - } + add_action( 'admin_menu', array( $this, 'admin_menu' ) ); + add_action( 'admin_init', array( $this, 'admin_init' ) ); + register_uninstall_hook( __FILE__, array( 'ff_meta', 'uninstall_hook' ) ); + } - function aux_get_all_locations() { - // gather all location data - if (false === ( $json_locs = get_transient("FF_metadata_json_locs") )) { - $all_locs = array(); - $arr_select = array('lat' => 1, 'lon' => 1); - foreach ($this->dir as $tmp_city => $url) { - try { - $tmp_meta = FF_Meta_Externaldata::get($url); - if (!empty($tmp_meta['location'])) { - $tmp_loc = array_intersect_key($tmp_meta['location'], $arr_select); - $all_locs[$tmp_city] = $tmp_loc; - } - } catch (Exception $e) { - // pass - } - } - $json_locs = json_encode($all_locs); - $cachetime = get_option('FF_meta_cachetime', FF_META_DEFAULT_CACHETIME) * MINUTE_IN_SECONDS; - set_transient("FF_metadata_json_locs", $json_locs, $cachetime); - } - return $json_locs; - } + private function aux_get_all_locations_json() { + if ( WP_DEBUG || ( false === ( $json_locs = get_transient( 'FF_metadata_json_locs' ) ) ) ) { + $all_locs = array(); + $comm_list = $this->dir->get_all_data(); + foreach ( $comm_list as $entry ) { + if ( isset( $entry['location'] ) + && isset( $entry['location']['lat'] ) + && isset( $entry['location']['lon'] ) + ) { + $all_locs[$entry['location']['city']] = array( + 'lat' => $entry['location']['lat'], + 'lon' => $entry['location']['lon'], + ); + } + } + $json_locs = json_encode( $all_locs ); + $cachetime = get_option( 'FF_meta_cachetime', FF_META_DEFAULT_CACHETIME ) * MINUTE_IN_SECONDS; + set_transient( 'FF_metadata_json_locs', $json_locs, $cachetime ); + } + return $json_locs; + } - function output_ff_location($citydata) { - // normal per-city code - $loc = new FF_Community($citydata); + function output_ff_state( $citydata ) { + if ( isset( $citydata['state'] ) && isset( $citydata['state']['nodes'] ) ) { + return sprintf( '%s', $citydata['state']['nodes'] ); + } else { + return ''; + } + } - $outstr = $loc->format_address(); - $json_locs = $this->aux_get_all_locations(); + function output_ff_location( $citydata ) { + // normal per-city code + $loc = new FF_Community( $citydata ); + $outstr = $loc->format_address(); + $json_locs = $this->aux_get_all_locations_json(); - if (!empty($loc_name) && !empty($loc_name)) { - $icon_url = plugin_dir_url(__FILE__) . "freifunk_marker.png"; - $outstr .= << + if ( ! empty( $loc->name ) && ! empty( $loc->name ) ) { + $icon_url = plugin_dir_url( __FILE__ ) . 'freifunk_marker.png'; + $loccity = $loc->city; + $outstr .= << EOT; - } - return $outstr; - } + } + return $outstr; + } - function output_ff_services($citydata) { - $outstr = ''; - return $outstr; - } + function output_ff_services( $citydata ) { + if ( ! isset( $citydata['services'] ) ) { + return ''; + } + $services = $citydata['services']; + $outstr = ''; + return $outstr; + } - function output_ff_contact($citydata) { - $outstr = '

'; - $contact = $citydata['contact']; - // Output -- rather ugly but the data is not uniform, some fields are URIs, some are usernames, ... - if (!empty($contact['email'])) { - $outstr .= sprintf("E-Mail: %s
\n", $contact['email'], $contact['email']); - } - if (!empty($contact['ml'])) { - $outstr .= sprintf("Mailingliste: %s
\n", $contact['ml'], $contact['ml']); - } - if (!empty($contact['irc'])) { - $outstr .= sprintf("IRC: %s
\n", $contact['irc'], $contact['irc']); - } - if (!empty($contact['twitter'])) { - // catch username instead of URI - if ($contact['twitter'][0] === "@") { - $twitter_url = 'http://twitter.com/' . ltrim($contact['twitter'], "@"); - $twitter_handle = $contact['twitter']; - } else { - $twitter_url = $contact['twitter']; - $twitter_handle = '@' . substr($contact['twitter'], strrpos($contact['twitter'], '/') + 1); - } - $outstr .= sprintf("Twitter: %s
\n", $twitter_url, $twitter_handle); - } - if (!empty($contact['facebook'])) { - $outstr .= sprintf("Facebook: %s
\n", $contact['facebook'], $contact['facebook']); - } - if (!empty($contact['googleplus'])) { - $outstr .= sprintf("G+: %s
\n", $contact['googleplus'], $contact['googleplus']); - } - if (!empty($contact['jabber'])) { - $outstr .= sprintf("XMPP: %s
\n", $contact['jabber'], $contact['jabber']); - } - $outstr .= '

'; - return $outstr; - } + function output_ff_contact( $citydata ) { + if ( ! isset( $citydata['contact'] ) ) { + return ''; + } + $contact = $citydata['contact']; + $outstr = '

'; + // Output -- rather ugly but the data is not uniform, + // some fields are URIs, some are usernames, ... + if ( ! empty( $contact['email'] ) ) { + $outstr .= sprintf( + 'E-Mail: %s
', + $contact['email'], $contact['email'] + ); + } + if ( ! empty( $contact['ml'] ) ) { + $outstr .= sprintf( + 'Mailingliste: %s
', + $contact['ml'], $contact['ml'] + ); + } + if ( ! empty( $contact['irc'] ) ) { + $outstr .= sprintf( + 'IRC: %s
', + $contact['irc'], $contact['irc'] + ); + } + if ( ! empty( $contact['twitter'] ) ) { + // catch username instead of URI + if ( $contact['twitter'][0] === '@' ) { + $twitter_url = 'http://twitter.com/' . ltrim( $contact['twitter'], '@' ); + $twitter_handle = $contact['twitter']; + } else { + $twitter_url = $contact['twitter']; + $twitter_handle = '@' . substr( + $contact['twitter'], strrpos( $contact['twitter'], '/' ) + 1 + ); + } + $outstr .= sprintf( + 'Twitter: %s
', + $twitter_url, $twitter_handle + ); + } + if ( ! empty( $contact['facebook'] ) ) { + $outstr .= sprintf( + 'Facebook: %s
', + $contact['facebook'], $contact['facebook'] + ); + } + if ( ! empty( $contact['googleplus'] ) ) { + $outstr .= sprintf( + 'G+: %s
', + $contact['googleplus'], $contact['googleplus'] + ); + } + if ( ! empty( $contact['jabber'] ) ) { + $outstr .= sprintf( + 'XMPP: %s
', + $contact['jabber'], $contact['jabber'] + ); + } + $outstr .= '

'; + return $outstr; + } - function output_ff_list() { - return 'here be some ff_list'; - } - - function shortcode_handler($atts, $content, $shortcode) { - // $atts[0] holds the city name, if given - if (empty($atts[0])) { - $city = get_option('FF_meta_city', FF_META_DEFAULT_CITY); - } else { - $city = $atts[0]; - } + function output_ff_list() { + $comm_list = $this->dir->get_all_data(); + $outstr = ''; + $outstr .= ''; + foreach ( $comm_list as $handle => $entry ) { + $outstr .= sprintf( + '', + esc_url( $entry['url'] ), + isset( $entry['name'] ) ? esc_html( $entry['name'] ) : esc_html($handle), + isset( $entry['location']['city'] ) ? esc_html( $entry['location']['city'] ) : 'n/a', + isset( $entry['state']['nodes'] ) ? esc_html( $entry['state']['nodes'] ) : 'n/a' + ); + } + $outstr .= '
NameStadtKnoten
%s%s%s
'; + return $outstr; + } + + function shortcode_handler( $atts, $content, $shortcode ) { + // $atts[0] holds the city name, if given + if ( empty( $atts[0] ) ) { + $city = get_option( 'FF_meta_city', FF_META_DEFAULT_CITY ); + } else { + $city = $atts[0]; + } - if (false === ($cityurl = $this->dir->get_url_by_city($city))) { - return "\n"; - } + if ( false === ( $cityurl = $this->dir->get_url_by_city( $city ) ) ) { + return "\n"; + } - if (false === ($metadata = FF_Meta_Externaldata::get($cityurl))) { - return "\n"; - } + $ed = new FF_Meta_Externaldata(); + if ( false === ( $metadata = $this->ed->get( $cityurl ) ) ) { + return "\n"; + } - $outstr = "
"; - switch ($shortcode) { - case 'ff_state': - $outstr .= $this->output_ff_state($metadata); - break; - case 'ff_location': - $outstr .= $this->output_ff_location($metadata); - break; - case 'ff_services': - $outstr .= $this->output_ff_services($metadata); - break; - case 'ff_contact': - $outstr .= $this->output_ff_contact($metadata); - break; - case 'ff_list': - $outstr .= $this->output_ff_list(); - break; - default: - $outstr .= ""; - break; - } - $outstr .= "
"; - return $outstr; - } + $outstr = "
"; + switch ( $shortcode ) { + case 'ff_state': + $outstr .= $this->output_ff_state( $metadata ); + break; + case 'ff_location': + $outstr .= $this->output_ff_location( $metadata ); + break; + case 'ff_services': + $outstr .= $this->output_ff_services( $metadata ); + break; + case 'ff_contact': + $outstr .= $this->output_ff_contact( $metadata ); + break; + case 'ff_list': + $outstr .= $this->output_ff_list(); + break; + default: + $outstr .= ''; + break; + } + $outstr .= '
'; + return $outstr; + } - function admin_menu() { - // Options Page: - add_options_page( - 'FF Meta Plugin', // page title - 'FF Meta', // menu title - 'manage_options', // req'd capability - 'ff_meta_plugin', // menu slug - array ('FF_meta', 'options_page') // callback function - ); - } + function admin_menu() { + // Options Page: + add_options_page( + 'FF Meta Plugin', // page title + 'FF Meta', // menu title + 'manage_options', // req'd capability + 'ff_meta_plugin', // menu slug + array( 'FF_meta', 'options_page' ) // callback function + ); + } - function admin_init() { - register_setting( - 'ff_meta_settings-group', // group name - 'ff_meta_cachetime' // option name - ); - register_setting( - 'ff_meta_settings-group', // group name - 'ff_meta_city' // option name - ); - add_settings_section( - 'ff_meta_section-one', // ID - 'Section One', // Title - array ('FF_Meta', 'section_one_callback'), // callback to fill - 'ff_meta_plugin' // page to display on - ); - add_settings_field( - 'ff_meta_city', // ID - 'Default community', // Title - array ('FF_Meta', 'city_callback'), // callback to fill field - 'ff_meta_plugin', // menu page=slug to display field on - 'ff_meta_section-one', // section to display the field in - array('label_for' => 'ff_meta_city_id') // ID of input element - ); - add_settings_field( - 'ff_meta_cachetime', // ID - 'Cache time', // Title - array ('FF_Meta', 'cachetime_callback'), // callback to fill field - 'ff_meta_plugin', // menu page=slug to display field on - 'ff_meta_section-one', // section to display the field in - array('label_for' => 'ff_meta_cachetime_id') // ID of input element - ); - } + function admin_init() { + register_setting( + 'ff_meta_settings-group', // group name + 'ff_meta_cachetime' // option name + ); + register_setting( + 'ff_meta_settings-group', // group name + 'ff_meta_city' // option name + ); + add_settings_section( + 'ff_meta_section-one', // ID + 'Section One', // Title + array( 'FF_Meta', 'section_one_callback' ), // callback to fill + 'ff_meta_plugin' // page to display on + ); + add_settings_field( + 'ff_meta_city', // ID + 'Default community', // Title + array( 'FF_Meta', 'city_callback' ), // callback to fill field + 'ff_meta_plugin', // menu page=slug to display field on + 'ff_meta_section-one', // section to display the field in + array( 'label_for' => 'ff_meta_city_id' ) // ID of input element + ); + add_settings_field( + 'ff_meta_cachetime', // ID + 'Cache time', // Title + array( 'FF_Meta', 'cachetime_callback' ), // callback to fill field + 'ff_meta_plugin', // menu page=slug to display field on + 'ff_meta_section-one', // section to display the field in + array( 'label_for' => 'ff_meta_cachetime_id' ) // ID of input element + ); + } - function section_one_callback() { - echo 'This Plugin provides shortcodes to display information from the Freifunk meta.json.'; - } + function section_one_callback() { + echo 'This Plugin provides shortcodes to display information' + .' from the Freifunk meta.json.'; + } - function cachetime_callback() { - $time = get_option( 'ff_meta_cachetime', FF_META_DEFAULT_CACHETIME ); - echo " minutes" - ."

Data from external URLs is cached for this number of minutes.

"; - } + function cachetime_callback() { + $time = get_option( 'ff_meta_cachetime', FF_META_DEFAULT_CACHETIME ); + echo 'Data from external URLs is cached' + .' for this number of minutes.

'; + } - function city_callback() { - if (false === ($directory = FF_Meta_Externaldata::get ( FF_META_DEFAULT_DIR ))) { - // TODO: error handling - return; - } - $default_city = get_option( 'ff_meta_city', FF_META_DEFAULT_CITY ); + function city_callback() { + $ed = new FF_Meta_Externaldata(); + if ( false === ( $directory = $this->ed->get( FF_META_DEFAULT_DIR ) ) ) { + // TODO: error handling + return; + } + $default_city = get_option( 'ff_meta_city', FF_META_DEFAULT_CITY ); - echo ""; - echo "

This is the default city parameter.

"; - } + echo "'; + echo '

This is the default city parameter.

'; + } - function options_page() { - ?> -
-

Freifunk Meta Plugin Options

-
- - - -
-
- +
+

Freifunk Meta Plugin Options

+
+ + + +
+
+ register_stuff(); +$GLOBALS['wp-plugin-ffmeta'] = $ffmeta; diff --git a/tests/bootstrap.php b/tests/bootstrap.php index c23c9cf..2a4de90 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -12,3 +12,4 @@ tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' ); require $_tests_dir . '/includes/bootstrap.php'; +require 'mock_ext_dataservice.php'; diff --git a/tests/example_directory.json b/tests/example_directory.json new file mode 100644 index 0000000..deea85c --- /dev/null +++ b/tests/example_directory.json @@ -0,0 +1,4 @@ +{ + "hamburg" : "http://example.org/ffhh.json", + "ffm" : "http://example.org/ffffm.json" +} \ No newline at end of file diff --git a/tests/mock_ext_dataservice.php b/tests/mock_ext_dataservice.php new file mode 100644 index 0000000..17e9e18 --- /dev/null +++ b/tests/mock_ext_dataservice.php @@ -0,0 +1,22 @@ +FFM = new FF_Meta(new MockDataService()); + $this->FFM->reinit_external_data_service(new MockDataService()); + } + + /* some very basic things */ + function test_basic_json_parsing() { + $json = file_get_contents(__DIR__.'/example_ffhh.json'); + $data = json_decode($json, $assoc = true); + + $this->assertArrayHasKey('name', $data); + $this->assertArrayHasKey('state', $data); + $this->assertArrayHasKey('location', $data); + $this->assertArrayHasKey('services', $data); + } + + function test_externaldata_mock() { + $ed = new MockDataService(); + $url_dir = 'https://raw.githubusercontent.com/freifunk/directory.api.freifunk.net/master/directory.json'; + $url_ff = 'http://meta.hamburg.freifunk.net/ffhh.json'; + $url_inv = 'http://meta.hamburg.freifunk.net/invalid.txt'; + + // verify that $ed->get does not read the URLs above, but local example_*.json files + $data_ff = $ed->get($url_ff); + $this->assertArrayHasKey('name', $data_ff); + $this->assertArrayHasKey('state', $data_ff); + $this->assertArrayHasKey('location', $data_ff); + $this->assertArrayHasKey('services', $data_ff); + + $data_dir = $ed->get($url_dir); + $this->assertArrayHasKey('hamburg', $data_dir); + $this->assertEquals(2, count($data_dir)); + + $data_inv = $ed->get($url_inv); + $this->assertEquals(0, count($data_inv)); + } + + /* the aux. classes */ + function test_ff_directory() { + $dir = new FF_Directory(new MockDataService()); + $valid = $dir->get_url_by_city('hamburg'); + $invalid = $dir->get_url_by_city('jena'); + + $this->assertTrue(!!$valid); + $this->assertTrue(!$invalid); + } + + /** + * @expectedException PHPUnit_Framework_Error + */ + function test_ff_community_invalid() { + $data = array(); + $comm = new FF_Community($data); + } + + function test_ff_community_empty() { + $data = array('location' => array()); + $comm = new FF_Community($data); + $this->assertEmpty($comm->street); + $this->assertEmpty($comm->name); + + $string = $comm->format_address(); + $this->assertEquals('', $string); + } + + function test_ff_community_filled() { + $data = array('location' => array( + 'address' => array( + 'Name' => 'some_name', + 'Street' => 'some_street', + 'Zipcode' => 'some_zip' + ), + 'city' => 'some_city', + 'lon' => 'some_lon', + 'lat' => 'some_lat', + )); + $comm = new FF_Community($data); + $this->assertEquals('some_name', $comm->name); + $this->assertEquals('some_street', $comm->street); + $this->assertEquals('some_zip', $comm->zip); + $this->assertEquals('some_city', $comm->city); + $this->assertEquals('some_lon', $comm->lon); + $this->assertEquals('some_lat', $comm->lat); + + $string = $comm->format_address(); + $this->assertEquals('

some_name
some_street
some_zip some_city

', $string); + } + + function test_ff_community_make_from_city() { + $comm = FF_Community::make_from_city('hamburg', new MockDataService()); + $this->assertEquals('Chaos Computer Club Hansestadt Hamburg', $comm->name); + $this->assertEquals('Humboldtstr. 53', $comm->street); + $this->assertEquals('22083', $comm->zip); + $this->assertEquals('Hamburg', $comm->city); + $this->assertEquals(10.024418, $comm->lon); + $this->assertEquals(53.574267, $comm->lat); + } + + /* the output methods */ + function test_output_ff_state_null() { + $data = array("state" => array("nodes" => null)); + $ret = $this->FFM->output_ff_state($data); + $this->assertEmpty($ret); + } + + function test_output_ff_state() { + $data = array("state" => array("nodes" => 429)); + $ret = $this->FFM->output_ff_state($data); + $this->assertRegExp('/429/', $ret); + } + + function test_output_ff_services_null() { + $data = array(); + $ret = $this->FFM->output_ff_services($data); + $this->assertEmpty($ret); + $this->assertEquals('', $ret); + } + + function test_output_ff_services() { + $data = array( + 'services' => array(array( + 'serviceName' => 'jabber', + 'serviceDescription' => 'chat', + 'internalUri' => 'xmpp://jabber.local', + ))); + $ret = $this->FFM->output_ff_services($data); + $this->assertEquals('', $ret); + } + + function test_output_ff_contact_null() { + $data = array(); + $ret = $this->FFM->output_ff_contact($data); + $this->assertEquals('', $ret); + } + + function test_output_ff_contact_filled() { + $data = array('contact' => array( + 'email' => 'mail@example.com', + 'jabber' => 'example@freifunk.net' + )); + $ret = $this->FFM->output_ff_contact($data); + $this->assertRegExp('/E-Mail/', $ret); + $this->assertRegExp('/mailto:mail@example\.com/', $ret); + $this->assertRegExp('/XMPP/', $ret); + $this->assertRegExp('/xmpp:example/', $ret); + + $data = array('contact' => array( + 'twitter' => 'http://twitter.com/freifunk' + )); + $ret = $this->FFM->output_ff_contact($data); + $this->assertRegExp('/twitter\.com\/freifunk/', $ret); + + $data = array('contact' => array( + 'twitter' => '@freifunk' + )); + $ret = $this->FFM->output_ff_contact($data); + $this->assertRegExp('/Twitter/', $ret); + $this->assertRegExp('/twitter\.com\/freifunk/', $ret); + + $data = array('contact' => array( + 'ml' => 'mail@example.com', + 'irc' => 'irc://irc.hackint.net/example', + 'facebook' => 'freifunk', + )); + $ret = $this->FFM->output_ff_contact($data); + $this->assertRegExp('/mailto:mail@example\.com/', $ret); + $this->assertRegExp('/irc\.hackint\.net\/example/', $ret); + $this->assertRegExp('/Facebook:/', $ret); + } +} diff --git a/tests/test-WpIntegrationTests.php b/tests/test-WpIntegrationTests.php new file mode 100644 index 0000000..d41dc20 --- /dev/null +++ b/tests/test-WpIntegrationTests.php @@ -0,0 +1,70 @@ +plugin = $GLOBALS['wp-plugin-ffmeta']; + $this->plugin->reinit_external_data_service(new MockDataService()); + } + + function test_post_ff_state() { + $post_content = '[ff_state]'; + $post_attribs = array( 'post_title' => 'Test', 'post_content' => $post_content ); + $post = $this->factory->post->create_and_get( $post_attribs ); + + // w/o filter: + $this->assertEquals($post_content, $post->post_content); + + // with filter: + $output = apply_filters( 'the_content', $post->post_content ); + $this->assertEquals("
429
\n", $output); + } + + function test_post_ff_state_othercity() { + $post_content = '[ff_state ffm]'; + $post_attribs = array( 'post_title' => 'Test', 'post_content' => $post_content ); + $post = $this->factory->post->create_and_get( $post_attribs ); + $output = apply_filters( 'the_content', $post->post_content ); + + $this->assertEquals("
\n", $output); + } + + function test_post_ff_state_inv_city() { + $post_content = '[ff_state jena]'; + $post_attribs = array( 'post_title' => 'Test', 'post_content' => $post_content ); + $post = $this->factory->post->create_and_get( $post_attribs ); + $output = apply_filters( 'the_content', $post->post_content ); + + $this->assertRegExp('/