├── .coveralls.yml ├── .gitignore ├── .travis.yml ├── README.md ├── README.txt ├── assets ├── banner-1544x500.jpg ├── banner-1544x500.png ├── banner-772x250-02.jpg ├── banner-772x250.png ├── screenshot-1.png ├── screenshot-2.png ├── screenshot-add.png ├── screenshot-pin.png ├── screenshot-remove.png ├── screenshot-zones.png ├── src │ ├── js │ │ └── script.js │ └── scss │ │ └── style.scss ├── stream_manager-icon.svg └── stream_manager-readme_banner.png ├── bin ├── install-wp-tests.sh └── phpunit-no-cover.xml ├── composer.json ├── composer.lock ├── gulpfile.js ├── includes ├── class-stream-manager-admin.php ├── class-stream-manager-ajax-helper.php ├── class-stream-manager-api.php ├── class-stream-manager-manager.php ├── class-stream-manager-utilities.php ├── class-stream-manager.php ├── timber-stream.php └── views │ ├── add.twig │ ├── meta.twig │ ├── rules.twig │ ├── stream.twig │ ├── stub.twig │ └── zones.twig ├── package.json ├── phpunit.xml ├── stream-manager.php └── tests ├── StreamManager_UnitTestCase.php ├── bootstrap.php ├── test-stream-manager-admin.php ├── test-stream-manager-ajax-helper.php ├── test-stream-manager-api.php ├── test-stream-manager-hooks.php └── test-stream-manager-integration.php /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .sass-cache 3 | vendor 4 | wp-content 5 | assets/build 6 | node_modules -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | dist: xenial 4 | 5 | services: mysql 6 | 7 | language: php 8 | 9 | php: 10 | - 5.6.30 11 | - 7.1 12 | 13 | env: 14 | - WP_VERSION=latest WP_MULTISITE=0 15 | - WP_VERSION=4.7.3 WP_MULTISITE=0 16 | 17 | before_script: 18 | - bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION 19 | - composer install 20 | 21 | script: 22 | - if [ "$TRAVIS_BRANCH" == "master" ]; then mkdir -p build/logs; vendor/bin/phpunit --coverage-clover build/logs/clover.xml; fi 23 | - if [ "$TRAVIS_BRANCH" != "master" ]; then vendor/bin/phpunit -c bin/phpunit-no-cover.xml; fi 24 | 25 | after_script: 26 | - if [ "$TRAVIS_BRANCH" == "master" ]; then php vendor/bin/coveralls -v; fi 27 | 28 | after_success: 29 | - if [ "$TRAVIS_BRANCH" == "master" ]; then coveralls; fi 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://github.com/Upstatement/stream-manager/blob/master/assets/stream_manager-readme_banner.png) 2 | 3 | [![Build Status](https://img.shields.io/travis/Upstatement/stream-manager/master.svg?style=flat-square)](https://travis-ci.org/Upstatement/stream-manager) 4 | [![Coverage Status](https://img.shields.io/coveralls/Upstatement/stream-manager.svg?style=flat-square)](https://coveralls.io/r/Upstatement/stream-manager?branch=master) 5 | 6 | 7 | Curate streams of WordPress posts. 8 | 9 | ## Setup 10 | 11 | Install and activate [Timber](https://github.com/jarednova/timber), then install and activate this plugin. Create your first stream in the WordPress admin, and then use this in your template file, replacing the ID with the stream ID: 12 | 13 | ```php 14 | $context['stream'] = new TimberStream(5); 15 | ``` 16 | 17 | And add this to your twig file: 18 | 19 | ```twig 20 | {% for post in stream.get_posts %} 21 | 22 | {{ post.title }} 23 | 24 | {% endfor %} 25 | ``` 26 | 27 | ## User Guide 28 | 29 | [Walkthrough Screencast](https://vimeo.com/160133857/025e8af0ae) 30 | 31 | ### Adding posts to a stream 32 | 33 | To add a post to the stream, start typing the title of the post in the 'Add Post' box. When the post you want to add appears, click it. The post should automatically be added to the top of the stream. 34 | 35 | ![](https://github.com/Upstatement/stream-manager/blob/master/assets/screenshot-add.png) 36 | 37 | ### Removing posts from a stream 38 | 39 | To remove a post from the stream, hover over the post and click the x in the upper right. Note that the post won't be deleted entirely -- instead, it will be removed from its current position and appended to the bottom of the stream. 40 | 41 | ![](https://github.com/Upstatement/stream-manager/blob/master/assets/screenshot-remove.png) 42 | 43 | ### Pinning posts 44 | 45 | Pinning a post will fix in in its current spot in the stream, even if new posts are added. For example, if you were to pin a post in the top slot, the next new post to be published will go to the second slot and the original post will remain at the top. Pin a post to its current spot by clicking on the thumbtack icon to the left of the post title. If the thumbtack is red, the post is pinned. Unpin a post by clicking the thumbtack a second time. 46 | 47 | ![](https://github.com/Upstatement/stream-manager/blob/master/assets/screenshot-pin.png) 48 | 49 | ### Reordering a stream 50 | 51 | Posts in the stream can be reordered via drag and drop. Make sure to click 'Update Post' after making changes to the stream. 52 | 53 | ### Using zones 54 | 55 | Zones are a useful tool for visualizing where posts are going to display on the page. For example, if the first post in the stream appears in a special featured slot, you might demarcate that using a zone title 'Featured Post.' To add a zone, type the name of the zone in the 'Zones' box on the right and click 'Add Zone.' The zone will be added to the top of the stream, after which it can be dragged and dropped to the desired location. 56 | 57 | ![](https://github.com/Upstatement/stream-manager/blob/master/assets/screenshot-zones.png) 58 | 59 | ## Filter Hooks 60 | 61 | Stream Manager includes several filter hooks that can be used to modify the options array attached to a stream. A common use case is to modify the query that populates the stream. 62 | 63 | ### Default Options 64 | 65 | ```php 66 | $default = array( 67 | 'query' => array( 68 | 'post_type' => 'post', 69 | 'post_status' => 'publish', 70 | 'has_password' => false, 71 | 'ignore_sticky_posts' => true, 72 | 'posts_per_page' => 100, 73 | 'orderby' => 'post__in' 74 | ), 75 | 76 | 'stream' => array(), 77 | 'layouts' => array( 78 | 'active' => 'default', 79 | 'layouts' => array( 80 | 'default' => array( 81 | 'name' => 'Default', 82 | 'zones' => array() 83 | ) 84 | ) 85 | ) 86 | ); 87 | ``` 88 | * * * 89 | 90 | ### stream-manager/options/id={stream-id} 91 | 92 | Restrict stream #3 to posts of the 'event' post type. 93 | 94 | ```php 95 | add_filter('stream-manager/options/id=3', function($defaults) { 96 | $defaults['query'] = array_merge($defaults['query'], array('post_type' => array('event'))); 97 | return $defaults; 98 | }); 99 | ``` 100 | 101 | * * * 102 | 103 | ### stream-manager/options/{stream-slug} 104 | 105 | Restrict the 'homepage' stream to posts in the 'local-news' category. 106 | 107 | ```php 108 | add_filter('stream-manager/options/homepage', function($defaults) { 109 | $defaults['query'] = array_merge($defaults['query'], array('category_name' => 'local-news')); 110 | return $defaults; 111 | }); 112 | ``` 113 | 114 | * * * 115 | 116 | ### stream-manager/taxonomy/{stream-slug} 117 | 118 | Restrict the 'classifieds' stream to the posts with the tags with term ids of 12 and 13 119 | 120 | ```php 121 | add_filter('stream-manager/taxonomy/classifieds', function($defaults) { 122 | $defaults['relation'] = "OR"; 123 | $defaults['post_tag'] = array( 12,13 ); 124 | return $defaults; 125 | }); 126 | ``` 127 | 128 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | === Stream Manager === 2 | Contributors: chrisvoll, lggorman, jarednova 3 | Tags: posts 4 | Requires at least: 3.8 5 | Tested up to: 4.7.3 6 | Stable tag: 1.3.4 7 | License: GPLv2 or later 8 | License URI: http://www.gnu.org/licenses/gpl-2.0.html 9 | 10 | Easily curate streams of recent posts. Pin, remove, or add posts to a stream via a drag and drop interface. 11 | 12 | == Description == 13 | 14 | We created Stream Manager with news editors in mind. Admins wanted the latest headlines to show up on the front page automatically, but didn’t want to give up the flexibility of pinning a major story in a featured spot or pushing a smaller item down below the fold. 15 | 16 | Stream Manager provides a simple interface for curating feeds of new posts from the WordPress Admin. New posts show up automatically at the top of a stream, but content can easily be added, removed, or repositioned on the page via the stream editor. Admins also have the option of pinning a post, which will lock it in its current position regardless of new content. 17 | 18 | Stream Manager is designed to work with Twig templating plugin [Timber](https://wordpress.org/plugins/timber-library/), as detailed in the installation instructions. Check out the [Timber project page](http://upstatement.com/timber/) for more info. 19 | 20 | = Links = 21 | * [Github repo](http://github.com/Upstatement/stream-manager) (includes user guide) 22 | * [Walkthough Screencast](https://vimeo.com/160133857/025e8af0ae) 23 | * [Developer docs](https://upstatement.github.io/stream-manager/) 24 | * [Timber docs](http://jarednova.github.io/timber/) 25 | 26 | == Installation == 27 | 28 | 1. Install and activate Timber, then install and activate this plugin. 29 | 2. Create a new stream from the WordPress admin. 30 | 3. Add the following to your template file, replacing 'new-stream' with the slug of your stream. 31 | ` 32 | $context['stream'] = new TimberStream('new-stream'); 33 | ` 34 | 4. Finally, add this to your twig file. 35 | ` 36 | {% for post in stream.get_posts %} 37 | 38 | {{ post.title }} 39 | 40 | {% endfor %} 41 | ` 42 | 43 | == Frequently Asked Questions == 44 | 45 | = Can streams be filtered by post type or category? = 46 | 47 | Yes! Streams can be filtered by post type, taxonomy, or just about anything else that can be passed into a wp_query array. Check out the [github readme](http://github.com/Upstatement/stream-manager) for details on filter hooks. 48 | 49 | 50 | == Screenshots == 51 | 52 | 1. Adding a new stream. 53 | 2. Pinning a stream to the top of 54 | 55 | -------------------------------------------------------------------------------- /assets/banner-1544x500.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Upstatement/stream-manager/3996657d7127146d11376418bf0cb7e279a03891/assets/banner-1544x500.jpg -------------------------------------------------------------------------------- /assets/banner-1544x500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Upstatement/stream-manager/3996657d7127146d11376418bf0cb7e279a03891/assets/banner-1544x500.png -------------------------------------------------------------------------------- /assets/banner-772x250-02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Upstatement/stream-manager/3996657d7127146d11376418bf0cb7e279a03891/assets/banner-772x250-02.jpg -------------------------------------------------------------------------------- /assets/banner-772x250.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Upstatement/stream-manager/3996657d7127146d11376418bf0cb7e279a03891/assets/banner-772x250.png -------------------------------------------------------------------------------- /assets/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Upstatement/stream-manager/3996657d7127146d11376418bf0cb7e279a03891/assets/screenshot-1.png -------------------------------------------------------------------------------- /assets/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Upstatement/stream-manager/3996657d7127146d11376418bf0cb7e279a03891/assets/screenshot-2.png -------------------------------------------------------------------------------- /assets/screenshot-add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Upstatement/stream-manager/3996657d7127146d11376418bf0cb7e279a03891/assets/screenshot-add.png -------------------------------------------------------------------------------- /assets/screenshot-pin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Upstatement/stream-manager/3996657d7127146d11376418bf0cb7e279a03891/assets/screenshot-pin.png -------------------------------------------------------------------------------- /assets/screenshot-remove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Upstatement/stream-manager/3996657d7127146d11376418bf0cb7e279a03891/assets/screenshot-remove.png -------------------------------------------------------------------------------- /assets/screenshot-zones.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Upstatement/stream-manager/3996657d7127146d11376418bf0cb7e279a03891/assets/screenshot-zones.png -------------------------------------------------------------------------------- /assets/src/js/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Stream Manager Admin JavaScript 3 | * 4 | * @package StreamManager 5 | * @author Chris Voll + Upstatement 6 | * @license GPL-2.0+ 7 | * @link http://upstatement.com 8 | * @copyright 2014 Upstatement 9 | */ 10 | 11 | jQuery(function($) { 12 | 13 | var stream = { 14 | 15 | //////////////////////////////////////////// 16 | // 17 | // Setup 18 | // 19 | //////////////////////////////////////////// 20 | 21 | $stream: $('.sm-posts'), 22 | $queue: $('.sm-alert'), 23 | $search: $('.sm-search'), 24 | $results: $('.sm-results'), 25 | $form: $('form#post'), 26 | 27 | init: function () { 28 | 29 | // stream Events 30 | $(document).on('heartbeat-tick.sm', $.proxy(this.on_heartbeat, this)); 31 | this.$stream 32 | .on('click', '.pin-unpin', $.proxy(this.on_stub_pin, this)) 33 | .on('dblclick', '.stub', $.proxy(this.on_stub_pin, this)) 34 | .on('click', '.remove', $.proxy(this.on_stub_remove, this)) 35 | .sortable({ 36 | start : $.proxy(this.on_sortable_start, this), 37 | stop : $.proxy(this.on_sortable_stop, this), 38 | change : $.proxy(this.on_sortable_change, this), 39 | revert : 150, 40 | axis : 'y' 41 | }); 42 | $('.reload-stream').on('click', $.proxy(this.on_stream_reload, this)); 43 | 44 | // stream Update Notifications 45 | $(document).on('sm/stream_update', $.proxy(this.on_stream_update, this)); 46 | this.$queue.on('click', $.proxy(this.on_apply_queue, this)); 47 | this.$form.on('submit.sm', $.proxy(this.on_form_submit, this)); 48 | 49 | // Search 50 | this.$search.on({ 51 | input: $.proxy(this.on_search_input, this), 52 | keydown: $.proxy(this.on_search_keydown, this), 53 | focus: $.proxy(this.on_show_results, this) 54 | }); 55 | this.$results.on({ 56 | mouseover: $.proxy(this.on_result_hover, this), 57 | 'click sm/select': $.proxy(this.on_result_select, this) 58 | }, '.sm-result'); 59 | $('body').on('mousedown', $.proxy(this.on_hide_results, this)); 60 | 61 | }, 62 | 63 | 64 | //////////////////////////////////////////// 65 | // 66 | // Heartbeat 67 | // 68 | // Avoid stream collisions by loading stream 69 | // updates from the database. For the purpose 70 | // of more accurate placements, pinned posts 71 | // are excluded from the list of IDs that 72 | // are passed around. 73 | // 74 | // Note that the purpose of this isn't to keep 75 | // the stream in sync among multiple editors; 76 | // instead, it's meant to ensure that no 77 | // published posts are left behind, in addition 78 | // to making sure that removed posts do not 79 | // interfere with the stream's sorting. 80 | // 81 | //////////////////////////////////////////// 82 | 83 | on_heartbeat: function(e, data) { 84 | var that = this; 85 | 86 | var front = this.$stream.attr('data-ids').split(',') 87 | back = data.sm_ids.split(','); 88 | 89 | // Published posts 90 | _.each( _.difference(back, front), function(id) { 91 | that.add_to_queue( 'insert', id, _.indexOf( back, id ) ); 92 | }); 93 | 94 | // Deleted posts 95 | _.each( _.difference(front, back), function(id) { 96 | that.add_to_queue( 'remove', id ); 97 | }); 98 | 99 | // Deleted pinned posts 100 | var front_pinned = this.$stream.attr('data-pinned').split(','), 101 | back_pinned = data.sm_pinned.split(','); 102 | 103 | _.each( _.difference(front_pinned, back_pinned), function(id) { 104 | that.add_to_queue( 'remove', id ); 105 | }); 106 | 107 | this.$stream.prop({ 108 | 'data-ids' : data.sm_ids, 109 | 'data-pinned' : data.sm_pinned 110 | }); 111 | }, 112 | 113 | 114 | //////////////////////////////////////////// 115 | // 116 | // Stream Manipulation 117 | // 118 | //////////////////////////////////////////// 119 | 120 | on_stub_pin: function(e) { 121 | e.preventDefault(); 122 | 123 | var $stub = $(e.target); 124 | if ( !$stub.is('.stub') ) $stub = $stub.closest('.stub'); 125 | 126 | if ( $stub.hasClass('zone') ) return; 127 | 128 | if ( $stub.hasClass('pinned') ) { 129 | $stub.removeClass('pinned'); 130 | $stub.find('.sm-pin-checkbox').prop('checked', false); 131 | $stub.find('.pin-unpin').prop('title', 'Pin this post') 132 | } else { 133 | $stub.addClass('pinned'); 134 | $stub.find('.sm-pin-checkbox').prop('checked', true); 135 | $stub.find('.pin-unpin') 136 | .prop('title', 'Unpin this post') 137 | .addClass('animating') 138 | .one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', function () { 139 | $(this).removeClass('animating'); 140 | }); 141 | } 142 | }, 143 | 144 | on_stub_remove: function(e) { 145 | e.preventDefault(); 146 | var $stub = $(e.target).closest('.stub'), 147 | that = this; 148 | 149 | if ( $stub.hasClass('zone') ) { 150 | $stub.remove(); 151 | $(document).trigger('sm/zone_update'); 152 | return; 153 | } 154 | if ( $stub.hasClass('removed') ) return; 155 | 156 | $stub.addClass('removed'); 157 | 158 | setTimeout(function() { 159 | that.remove_single( $stub.attr('data-id') ); 160 | }, 500); 161 | }, 162 | 163 | on_sortable_start: function (event, ui) { 164 | $(document).trigger('sm/sortable_start', ui.item); 165 | $(ui.placeholder).height($(ui.item).height()); 166 | if ( ui.item.hasClass('pinned') ) { 167 | if ( !ui.item.hasClass('zone') ) { 168 | this.inventory_pinned('zone'); 169 | } else { 170 | this.pinned_inventory = []; 171 | } 172 | } else { 173 | this.inventory_pinned(); 174 | } 175 | }, 176 | 177 | on_sortable_stop: function (event, ui) { 178 | if ( ui.item.hasClass('zone') ) { 179 | $(document).trigger('sm/zone_update'); 180 | } 181 | }, 182 | 183 | on_sortable_change: function (event, ui) { 184 | this.remove_pinned(); 185 | this.insert_pinned(); 186 | }, 187 | 188 | 189 | // Remove all unpinned items, get new posts 190 | // from the database based on categories and tags 191 | // selected under Rules 192 | on_stream_reload: function (e) { 193 | e.preventDefault(); 194 | var that = this; 195 | 196 | // Clear queues 197 | this.insert_queue = []; 198 | this.remove_queue = []; 199 | $(document).trigger('sm/stream_update'); 200 | 201 | // Disable heartbeat checking 202 | $(document).off('heartbeat-tick.sm'); 203 | 204 | 205 | // Setup the ajax request 206 | var categories = []; 207 | $('#categorychecklist input:checked').each(function() { 208 | categories.push( $(this).val() ); 209 | }) 210 | var exclude = []; 211 | $('.stub.pinned').each(function() { 212 | exclude.push( $(this).attr('data-id') ); 213 | }); 214 | var request = { 215 | action: 'sm_reload', 216 | stream_id: $('#post_ID').val(), 217 | taxonomies: { 218 | category: categories, 219 | post_tag: $('#tax-input-post_tag').val(), 220 | }, 221 | exclude: exclude 222 | }; 223 | 224 | $.post(ajaxurl, request, function(response) { 225 | response = JSON.parse(response); 226 | if ( !response.status || response.status == 'error' ) return; 227 | 228 | that.inventory_pinned(); 229 | that.remove_pinned(); 230 | that.$stream.empty(); 231 | $(response.data).each(function() { 232 | that.$stream.append(this); 233 | }); 234 | that.insert_pinned(); 235 | }); 236 | }, 237 | 238 | 239 | 240 | //////////////////////////////////////////// 241 | // 242 | // Post queue UI 243 | // 244 | // Listens for the stream events to update 245 | // the user interface, letting the end user 246 | // know when there are changes. 247 | // 248 | //////////////////////////////////////////// 249 | 250 | on_stream_update: function (e) { 251 | var insert = this.insert_queue, 252 | remove = this.remove_queue; 253 | 254 | if ( (insert.length + remove.length) > 0 ) { 255 | this.$queue.show(); 256 | var text = [' ']; 257 | 258 | if ( insert.length == 1 ) { 259 | text.push('There is 1 new post. '); 260 | } else if ( insert.length > 1 ) { 261 | text.push('There are ' + insert.length + ' new posts. '); 262 | } 263 | 264 | if ( remove.length == 1 ) { 265 | text.push('There is 1 removed post.'); 266 | } else if ( remove.length > 1 ) { 267 | text.push('There are ' + remove.length + ' removed posts.'); 268 | } 269 | 270 | this.$queue.html(text.join("")); 271 | this.allow_submit = false; 272 | } else { 273 | this.$queue.hide(); 274 | this.allow_submit = true; 275 | } 276 | }, 277 | 278 | on_apply_queue: function(e) { 279 | this.apply_queues(); 280 | this.allow_submit = true; 281 | }, 282 | 283 | // @TODO: force one more heartbeat 284 | on_form_submit: function(e) { 285 | this.submit_flag = !this.submit_flag; 286 | if ( !this.submit_flag && !this.allow_submit ) return; 287 | 288 | if ( !this.allow_submit ) { 289 | if ( ! window.confirm('New posts have been published or removed since you began editing the stream. \n\nPress Cancel to go back, or OK to save the stream without them.') ) { 290 | e.preventDefault(); 291 | } else { 292 | this.allow_submit = true; 293 | } 294 | } 295 | }, 296 | 297 | 298 | //////////////////////////////////////////// 299 | // 300 | // Post Queue 301 | // 302 | // API for modifying the posts in the stream 303 | // user interface, including adding and 304 | // removing by post IDs. Used by the collision 305 | // management system (with the WordPress 306 | // heartbeat API) and the post search. 307 | // 308 | // ----------------------------------------- 309 | // 310 | // Usage: (where queue_name is 'insert' or 'remove') 311 | // > stream.add_to_queue( queue_name, post_id, position ); 312 | // > stream.remove_from_queue( queue_name, post_id ); 313 | // 314 | // Apply changes: 315 | // > stream.apply_insert( queue_override ); 316 | // > stream.apply_remove( queue_override ); 317 | // 318 | // Add a single post without invoking the queues: 319 | // > stream.insert_single( id, position ); 320 | // > stream.remove_single( id ); 321 | // 322 | //////////////////////////////////////////// 323 | 324 | insert_queue: [], 325 | remove_queue: [], 326 | 327 | /** 328 | * Add a post to a queue 329 | */ 330 | add_to_queue: function ( queue, id, position ) { 331 | if ( queue == 'insert' ) { 332 | if ( this.post_exists(id) || this.is_in_queue('insert', id) !== -1 ) return; 333 | this.insert_queue.push({ 334 | id: id, 335 | position: position 336 | }); 337 | } else if ( queue == 'remove' ) { 338 | if ( !this.post_exists(id) || this.is_in_queue('remove', id) !== -1 ) return; 339 | this.remove_queue.push({ 340 | id: id 341 | }); 342 | } 343 | $(document).trigger('sm/stream_update'); 344 | }, 345 | 346 | /** 347 | * Remove a post from a queue 348 | */ 349 | remove_from_queue: function ( queue, id ) { 350 | var queue_name = queue + '_queue'; 351 | 352 | for ( i in this[queue_name] ) { 353 | if ( this[queue_name][i].id == id ) { 354 | this[queue_name].splice(i, 1); 355 | } 356 | } 357 | $(document).trigger('sm/stream_update'); 358 | }, 359 | 360 | /** 361 | * Checks if a post is in a queue 362 | * @return -1 if not found, position if found 363 | */ 364 | is_in_queue: function ( queue, id ) { 365 | var queue_name = queue + '_queue'; 366 | 367 | for ( i in this[queue_name] ) { 368 | if ( this[queue_name][i].id == id ) { 369 | return i; 370 | } 371 | } 372 | return -1; 373 | }, 374 | 375 | 376 | /** 377 | * Apply queue changes 378 | */ 379 | apply_insert: function ( queue_override, animate ) { 380 | var queue = queue_override ? queue_override : this.insert_queue; 381 | if (queue.length < 1) return; 382 | var that = this; 383 | 384 | $.post(ajaxurl, { 385 | action: 'sm_request', 386 | queue: queue 387 | }, function (response) { 388 | response = JSON.parse(response); 389 | if ( response.status && response.status == 'error' ) return; 390 | that.ui_insert( response.data, animate ); 391 | $(document).trigger( 'sm/stream_update' ); 392 | } 393 | ); 394 | }, 395 | apply_remove: function ( queue_override ) { 396 | var queue = queue_override ? queue_override : this.remove_queue; 397 | if (queue.length < 1) return; 398 | this.ui_remove( queue ); 399 | $(document).trigger( 'sm/stream_update' ); 400 | }, 401 | apply_queues: function () { 402 | this.apply_remove(); 403 | this.apply_insert(null, true); 404 | }, 405 | 406 | 407 | 408 | /** 409 | * Insert/remove post(s) in the UI 410 | */ 411 | ui_insert: function ( insert_data, animate ) { 412 | if ( !insert_data ) return; 413 | 414 | this.inventory_pinned(); 415 | this.remove_pinned(); 416 | 417 | for ( id in insert_data ) { 418 | if ( insert_data[id]['object'] ) { 419 | this.inject( insert_data[id]['position'], insert_data[id]['object'], animate ); 420 | } 421 | this.remove_from_queue( 'insert', id ); 422 | } 423 | 424 | this.insert_pinned(); 425 | }, 426 | ui_remove: function ( remove_queue ) { 427 | if ( !remove_queue ) return; 428 | 429 | this.inventory_pinned(); 430 | this.remove_pinned(); 431 | 432 | this.delete_pinned( remove_queue ); 433 | 434 | for ( i in remove_queue ) { 435 | var id = remove_queue[i].id; 436 | this.$stream.find('#post-' + id).remove(); 437 | this.remove_from_queue( 'remove', id ); 438 | } 439 | 440 | this.insert_pinned(); 441 | }, 442 | 443 | 444 | 445 | /** 446 | * Insert or remove just one post without invoking the queues 447 | */ 448 | insert_single: function ( id, position ) { 449 | if ( this.post_exists(id) ) return; 450 | this.apply_insert([{ 451 | id: id, 452 | position: position 453 | }], true); 454 | }, 455 | remove_single: function ( id ) { 456 | if ( !this.post_exists(id) ) return; 457 | this.apply_remove([{ 458 | id: id 459 | }]); 460 | }, 461 | 462 | 463 | /** 464 | * Inserts one post object into the stream 465 | */ 466 | inject: function (position, object, animate) { 467 | object = $( object ); 468 | if ( position == 0 ) { 469 | this.$stream.prepend( object ); 470 | } else { 471 | var $object_before = this.$stream.find( '.stub:nth-child(' + position + ')' ); 472 | if ( $object_before.length ) { 473 | $object_before.after( object ); 474 | } else { 475 | this.$stream.append( object ); 476 | } 477 | } 478 | 479 | if ( animate ) { 480 | object.addClass('inserted'); 481 | setTimeout(function() { 482 | object.removeClass('inserted'); 483 | }, 2000); 484 | } 485 | }, 486 | 487 | 488 | /** 489 | * Check if a post exists in the stream 490 | */ 491 | find_post: function (id) { 492 | return this.$stream.find('#post-' + id); 493 | }, 494 | post_exists: function (id) { 495 | return this.find_post(id).length; 496 | }, 497 | 498 | 499 | /** 500 | * Helpers for keeping pinned stubs in place 501 | */ 502 | pinned_inventory: [], 503 | inventory_pinned: function (className) { 504 | if ( !className ) className = 'pinned'; 505 | var that = this; 506 | this.pinned_inventory = []; 507 | this.$stream.find('.stub').each( function (i) { 508 | if ( $(this).hasClass( className ) ) { 509 | that.pinned_inventory.push({ 510 | id: $(this).attr('data-id'), 511 | obj: this, 512 | position: i 513 | }); 514 | } 515 | }); 516 | }, 517 | remove_pinned: function () { 518 | for (i in this.pinned_inventory) { 519 | this.pinned_inventory[i].obj.remove(); 520 | } 521 | }, 522 | delete_pinned: function ( remove_queue ) { 523 | for ( i in this.pinned_inventory ) { 524 | var id = this.pinned_inventory[i].id; 525 | for ( j in remove_queue ) { 526 | if ( remove_queue[j].id == id ) { 527 | this.remove_from_queue( 'remove', id ); 528 | this.pinned_inventory.splice(i, 1); 529 | } 530 | } 531 | } 532 | }, 533 | insert_pinned: function () { 534 | for (i in this.pinned_inventory) { 535 | this.inject( 536 | this.pinned_inventory[i].position, 537 | this.pinned_inventory[i].obj 538 | ); 539 | } 540 | }, 541 | 542 | 543 | //////////////////////////////////////////// 544 | // 545 | // Search 546 | // 547 | //////////////////////////////////////////// 548 | 549 | search_query: '', 550 | search_timer: null, 551 | 552 | allow_submit: true, 553 | submit_flag: true, // because `submit` event gets called twice 554 | 555 | on_search_input: function(e) { 556 | var that = this; 557 | 558 | clearTimeout(this.search_timer); 559 | this.search_timer = setTimeout(function() { 560 | if ( $(e.target).val() !== that.search_query ) { 561 | that.search_query = $(e.target).val(); 562 | 563 | if ( that.search_query.length > 2 ) { 564 | 565 | var request = { 566 | action: 'sm_search', 567 | query: that.search_query, 568 | stream_id: $('#post_ID').val() 569 | }; 570 | 571 | $.post(ajaxurl, request, function(results) { 572 | var data = JSON.parse(results); 573 | 574 | that.$results.empty(); 575 | that.$results.show(); 576 | 577 | for (i in data.data) { 578 | var post = data.data[i]; 579 | that.$results.append([ 580 | '
  • ', 581 | '', 582 | post.title, 583 | ' ', 584 | post.human_date + ' ago', 585 | that.post_exists( post.id ) ? ' - Already in stream' : '', 586 | '', 587 | '', 588 | '
  • '].join('') 589 | ); 590 | 591 | that.$results.find('li:nth-child(1) .sm-result').addClass('active'); 592 | } 593 | }); 594 | } else { 595 | that.$results.empty(); 596 | that.$results.hide(); 597 | } 598 | } 599 | }, 200); 600 | }, 601 | 602 | on_search_keydown: function (e) { 603 | if (e.keyCode == 38) { 604 | // up 605 | e.preventDefault(); 606 | this.$results.show(); 607 | var $active = this.$results.find('.active'); 608 | var $prev = $active.parent().prev().find('.sm-result'); 609 | 610 | if (!$prev.length) return; 611 | 612 | $active.removeClass('active'); 613 | $prev.addClass('active'); 614 | } else if (e.keyCode == 40) { 615 | // down 616 | e.preventDefault(); 617 | this.$results.show(); 618 | var $active = this.$results.find('.active'); 619 | var $next = $active.parent().next().find('.sm-result'); 620 | 621 | if (!$next.length) return; 622 | 623 | $active.removeClass('active'); 624 | $next.addClass('active'); 625 | } else if (e.keyCode == 13) { 626 | // enter 627 | e.preventDefault(); 628 | if ( !this.$results.is(':visible') ) return; 629 | this.$results.find('.active').trigger('sm/select'); 630 | } 631 | }, 632 | 633 | on_show_results: function(e) { 634 | if ( !this.$results.is(':empty') ) { 635 | this.$results.show(); 636 | } 637 | }, 638 | 639 | on_result_hover: function (e) { 640 | if ( $(e.currentTarget).hasClass('active') ) return; 641 | this.$results.find('.active').removeClass('active'); 642 | $(e.currentTarget).addClass('active'); 643 | }, 644 | 645 | on_result_select: function (e) { 646 | e.preventDefault(); 647 | var id = $(e.currentTarget).attr('data-id'); 648 | var current = this.find_post( id ); 649 | if ( current && current.length ) { 650 | // only move non-pinned item 651 | // @TODO: revisit this in the future 652 | if ( current.hasClass('pinned') ) { 653 | setTimeout(function() { alert('This post is already pinned in the stream. To move it, please unpin it first.'); }, 0); 654 | return false; 655 | } 656 | this.remove_single( id ); 657 | } 658 | this.insert_single( id, 0 ); 659 | this.$results.hide(); 660 | }, 661 | 662 | on_hide_results: function(e) { 663 | if ( !$(e.target).closest('.sm-search-container').length ) { 664 | this.$results.hide(); 665 | } 666 | }, 667 | 668 | }; 669 | 670 | 671 | 672 | 673 | stream.layouts = { 674 | 675 | data: {}, 676 | 677 | init: function() { 678 | this.$data_field = $('.layouts-data'); 679 | this.$container = $('#stream_box_zones'); 680 | this.data = JSON.parse( this.$data_field.val() ); 681 | $(document).on('sm/zone_update', $.proxy( this.on_zone_update, this )); 682 | 683 | // this.$container.find('.add-zone, .add-layout').on('click', this.on_toggle_add ); 684 | this.$container.find('.add-zone-input').on('keydown', $.proxy( this.on_add_keydown, this )); 685 | this.$container.find('.add-zone-button').on('click', $.proxy( this.on_click_add_button, this )); 686 | // this.$container.find('.active-layout').on('change', $.proxy( this.on_change_layout, this )); 687 | stream.$stream.on({ 688 | input : $.proxy( this.on_zone_update, this ), 689 | keydown : this.on_zone_keydown 690 | }, '.zone .zone-header'); 691 | }, 692 | 693 | on_toggle_add: function(e) { 694 | e.preventDefault(); 695 | // if ( $(this).hasClass('add-zone') ) { 696 | $(this).siblings('.add-zone-container').toggle(); 697 | // } 698 | // if ( $(this).hasClass('add-layout') ) { 699 | // $(this).siblings('.add-layout-container').toggle(); 700 | // } 701 | }, 702 | 703 | on_zone_update: function() { 704 | var that = this; 705 | // Update the internal data 706 | var active = this.data.active; 707 | this.data.layouts[active].zones = []; 708 | 709 | stream.$stream.find('.zone').each(function(index, el) { 710 | that.data.layouts[active].zones.push({ 711 | position: $(this).index(), 712 | title: $(this).find('.zone-header').val() 713 | }); 714 | }); 715 | 716 | var $select = this.$container.find('.active-layout'); 717 | $select.empty(); 718 | for ( i in this.data.layouts ) { 719 | $select.append([ 720 | '' 723 | ].join("")); 724 | } 725 | 726 | this.$data_field.val( JSON.stringify(this.data) ); 727 | }, 728 | 729 | on_zone_keydown: function (e) { 730 | // disable enter 731 | if ( e.keyCode == '13' ) { 732 | e.preventDefault(); 733 | this.blur(); 734 | } 735 | }, 736 | 737 | on_click_add_button: function (e) { 738 | e.preventDefault(); 739 | //if ( $(e.currentTarget).hasClass('add-zone-button') ) { 740 | var $input = $(e.currentTarget).siblings('.layouts-input'); 741 | this.insert_zones([{ 742 | position: 0, 743 | title: $input.val() 744 | }]); 745 | $input.val(''); 746 | //} 747 | 748 | // if ( $(e.currentTarget).hasClass('add-layout-button') ) { 749 | // var $input = $(e.currentTarget).siblings('.layouts-input'); 750 | // var slug = this.slugify( $input.val() ); 751 | // this.data.layouts[ slug ] = { 752 | // name: $input.val(), 753 | // zones: {} 754 | // } 755 | // this.data.active = slug; 756 | // $(document).trigger('sm/zone_update'); 757 | // $input.val(''); 758 | // } 759 | }, 760 | on_add_keydown: function (e) { 761 | if ( e.keyCode == '13' ) { 762 | e.preventDefault(); 763 | $(e.currentTarget).siblings('.button').trigger('click'); 764 | } 765 | }, 766 | 767 | // on_change_layout: function (e) { 768 | // this.data.active = $(e.currentTarget).val(); 769 | // this.remove_zones(); 770 | // this.insert_zones( this.data.layouts[ this.data.active ].zones ); 771 | // }, 772 | 773 | insert_zones: function( zones ) { 774 | for ( i in zones ) { 775 | stream.inject( zones[i].position, $([ 776 | '
    ', 777 | '', 778 | '', 779 | '
    ' 780 | ].join("")) ); 781 | } 782 | $(document).trigger('sm/zone_update'); 783 | }, 784 | 785 | remove_zones: function() { 786 | stream.$stream.find('.zone').remove(); 787 | }, 788 | 789 | // slugify: function(name) { 790 | // return name.toLowerCase().replace(/ /g,'-').replace(/[-]+/g, '-').replace(/[^\w-]+/g,''); 791 | // } 792 | }; 793 | 794 | 795 | 796 | 797 | stream.init(); 798 | stream.layouts.init(); 799 | 800 | window.stream = stream; 801 | }); 802 | -------------------------------------------------------------------------------- /assets/src/scss/style.scss: -------------------------------------------------------------------------------- 1 | 2 | //////////////////////////////////////////// 3 | // 4 | // Configuration 5 | // 6 | //////////////////////////////////////////// 7 | 8 | $stub-hover: #FAFAFA; 9 | $stub-pinned: #F1F1F1; 10 | 11 | 12 | //////////////////////////////////////////// 13 | // 14 | // Mixins & Animations 15 | // 16 | //////////////////////////////////////////// 17 | 18 | @mixin prefixer($prefix, $content) { 19 | -webkit-#{$prefix}: #{$content}; 20 | -moz-#{$prefix}: #{$content}; 21 | #{$prefix}: #{$content}; 22 | } 23 | @mixin clear { 24 | &::after { 25 | clear: both; 26 | content: ""; 27 | display: table; 28 | } 29 | } 30 | @-webkit-keyframes pulse { 31 | 0% { 32 | -webkit-transform: scale3d(1, 1, 1); 33 | transform: scale3d(1, 1, 1); 34 | } 35 | 30% { 36 | -webkit-transform: scale3d(1.4, 1.4, 1.4); 37 | transform: scale3d(1.4, 1.4, 1.4); 38 | } 39 | 60% { 40 | -webkit-transform: scale3d(.8, .8, .8); 41 | transform: scale3d(.8, .8, .8); 42 | } 43 | 100% { 44 | -webkit-transform: scale3d(1, 1, 1); 45 | transform: scale3d(1, 1, 1); 46 | } 47 | } 48 | @keyframes pulse { 49 | 0% { 50 | -webkit-transform: scale3d(1, 1, 1); 51 | -ms-transform: scale3d(1, 1, 1); 52 | transform: scale3d(1, 1, 1); 53 | } 54 | 30% { 55 | -webkit-transform: scale3d(1.4, 1.4, 1.4); 56 | -ms-transform: scale3d(1.4, 1.4, 1.4); 57 | transform: scale3d(1.4, 1.4, 1.4); 58 | } 59 | 60% { 60 | -webkit-transform: scale3d(.8, .8, .8); 61 | -ms-transform: scale3d(.8, .8, .8); 62 | transform: scale3d(.8, .8, .8); 63 | } 64 | 100% { 65 | -webkit-transform: scale3d(1, 1, 1); 66 | -ms-transform: scale3d(1, 1, 1); 67 | transform: scale3d(1, 1, 1); 68 | } 69 | } 70 | 71 | 72 | //////////////////////////////////////////// 73 | // 74 | // Post Stub 75 | // 76 | //////////////////////////////////////////// 77 | 78 | .stub { 79 | cursor: move; 80 | padding: 10px; 81 | background: #FFF; 82 | margin: 0 1px 1px; 83 | box-shadow: 0 1px 1px rgba(0, 0, 0, .1); 84 | @include prefixer(transition, .2s background); 85 | @include clear; 86 | min-height: 72px; 87 | a { 88 | text-decoration: none; 89 | } 90 | 91 | .post-thumb { 92 | float: right; 93 | margin-right: 10px; 94 | margin-left:10px; 95 | } 96 | 97 | .stub-action { 98 | display: inline-block; 99 | padding: 10px 10px 10px 5px; 100 | opacity: .1; 101 | margin-bottom: 25px; 102 | @include prefixer(transition, .2s opacity); 103 | 104 | 105 | &.pin-unpin { 106 | float: left; 107 | } 108 | &.remove { 109 | float: right; 110 | opacity: 0; 111 | } 112 | } 113 | 114 | .date { 115 | color: #AAA; 116 | border: 0; 117 | } 118 | .row-actions { 119 | opacity: 0; 120 | display: inline; 121 | visibility: visible; 122 | @include prefixer(transition, .2s opacity); 123 | } 124 | 125 | &:hover { 126 | background: $stub-hover; 127 | .row-actions { 128 | opacity: 1; 129 | } 130 | .stub-action { 131 | opacity: .5; 132 | } 133 | } 134 | 135 | &.pinned { 136 | box-shadow: none; 137 | 138 | &:hover { 139 | background: darken($stub-pinned, 3%); 140 | } 141 | .pin-unpin { 142 | opacity: 1; 143 | color: #D54E21; 144 | &.animating { 145 | -webkit-animation: pulse .5s ease-out; 146 | animation: pulse .5s ease-out; 147 | } 148 | } 149 | } 150 | &.ui-sortable-helper { 151 | box-shadow: 0 2px 10px rgba(0, 0, 0, .1); 152 | opacity: .9; 153 | } 154 | &.inserted { 155 | background: #FBF0C0; 156 | } 157 | &.removed { 158 | background: #FBCFCA !important; 159 | } 160 | } 161 | 162 | 163 | //////////////////////////////////////////// 164 | // 165 | // Stream Queue UI 166 | // 167 | //////////////////////////////////////////// 168 | 169 | .sm-alert { 170 | display: none; 171 | cursor: pointer; 172 | padding: 8px 12px; 173 | background: #1E8CBE; 174 | color: #FFF; 175 | font-weight: bold; 176 | } 177 | 178 | 179 | //////////////////////////////////////////// 180 | // 181 | // Search 182 | // 183 | //////////////////////////////////////////// 184 | 185 | .sm-add { 186 | 187 | .sm-search-container { 188 | position: relative; 189 | } 190 | 191 | .sm-search { 192 | display: block; 193 | width: 100%; 194 | padding: 10px 14px; 195 | margin: 0; 196 | } 197 | 198 | .sm-results { 199 | padding: 5px 0; 200 | margin: 0; 201 | position: absolute; 202 | top: 100%; 203 | left: 0; 204 | z-index: 100; 205 | background: #FFF; 206 | border-radius: 0 0 4px 4px; 207 | box-shadow: 0 3px 7px rgba(0, 0, 0, .1); 208 | 209 | li { 210 | margin: 0; 211 | } 212 | 213 | .sm-result { 214 | text-decoration: none; 215 | display: block; 216 | padding: 6px 15px; 217 | 218 | &.active { 219 | background: #0074A2; 220 | color: #FFF; 221 | } 222 | } 223 | 224 | .sm-result-date { 225 | font-size: 11px; 226 | opacity: .5; 227 | margin-left: 5px; 228 | } 229 | } 230 | } 231 | 232 | 233 | //////////////////////////////////////////// 234 | // 235 | // Layouts & Zones 236 | // 237 | //////////////////////////////////////////// 238 | 239 | .zone { 240 | margin: 0; 241 | box-shadow: none; 242 | min-height: 0; 243 | display:block; 244 | background: transparent; 245 | .stub-action { 246 | margin-bottom: 0px; 247 | } 248 | .zone-header { 249 | border-width: 0; 250 | background: transparent; 251 | box-shadow: none; 252 | margin: 6px 1px; 253 | 254 | &:focus { 255 | background: #FFF; 256 | border-width: 1px; 257 | margin: 5px 0; 258 | } 259 | } 260 | 261 | h3 { 262 | margin: 0; 263 | } 264 | } 265 | 266 | .add-layout-container, 267 | .add-zone-container { 268 | display: none; 269 | } 270 | 271 | 272 | 273 | // Override WordPress edit page styles 274 | #stream_box_stream { 275 | background: transparent; 276 | border: 0; 277 | @include prefixer(box-shadow, none); 278 | 279 | > .handlediv, 280 | > .hndle { 281 | display: none; 282 | } 283 | 284 | > .inside { 285 | margin: 0; 286 | padding: 0; 287 | 288 | > .sm-posts { 289 | border: 0; 290 | } 291 | } 292 | } 293 | .post-type-sm_stream { 294 | #title { 295 | position: relative; 296 | z-index: 1; 297 | } 298 | } 299 | .misc-pub-visibility, 300 | .misc-pub-curtime { 301 | display: none; 302 | } 303 | #misc-publishing-actions { 304 | padding-bottom: 15px; 305 | } 306 | -------------------------------------------------------------------------------- /assets/stream_manager-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/stream_manager-readme_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Upstatement/stream-manager/3996657d7127146d11376418bf0cb7e279a03891/assets/stream_manager-readme_banner.png -------------------------------------------------------------------------------- /bin/install-wp-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ $# -lt 3 ]; then 4 | echo "usage: $0 [db-host] [wp-version]" 5 | exit 1 6 | fi 7 | 8 | DB_NAME=$1 9 | DB_USER=$2 10 | DB_PASS=$3 11 | DB_HOST=${4-localhost} 12 | WP_VERSION=${5-latest} 13 | 14 | WP_TESTS_DIR=${WP_TESTS_DIR-/tmp/wordpress-tests-lib} 15 | WP_CORE_DIR=${WP_CORE_DIR-/tmp/wordpress/} 16 | 17 | echo $WP_TESTS_DIR; 18 | 19 | download() { 20 | if [ `which curl` ]; then 21 | curl -s "$1" > "$2"; 22 | elif [ `which wget` ]; then 23 | wget -nv -O "$2" "$1" 24 | fi 25 | } 26 | 27 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+(\.[0-9]+)? ]]; then 28 | WP_TESTS_TAG="tags/$WP_VERSION" 29 | else 30 | # http serves a single offer, whereas https serves multiple. we only want one 31 | download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json 32 | grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json 33 | LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') 34 | if [[ -z "$LATEST_VERSION" ]]; then 35 | echo "Latest WordPress version could not be found" 36 | exit 1 37 | fi 38 | WP_TESTS_TAG="tags/$LATEST_VERSION" 39 | fi 40 | 41 | set -ex 42 | 43 | install_wp() { 44 | 45 | if [ -d $WP_CORE_DIR ]; then 46 | return; 47 | fi 48 | 49 | mkdir -p $WP_CORE_DIR 50 | 51 | if [ $WP_VERSION == 'latest' ]; then 52 | local ARCHIVE_NAME='latest' 53 | else 54 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 55 | fi 56 | 57 | download https://wordpress.org/${ARCHIVE_NAME}.tar.gz /tmp/wordpress.tar.gz 58 | tar --strip-components=1 -zxmf /tmp/wordpress.tar.gz -C $WP_CORE_DIR 59 | 60 | download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php 61 | } 62 | 63 | install_test_suite() { 64 | # portable in-place argument for both GNU sed and Mac OSX sed 65 | if [[ $(uname -s) == 'Darwin' ]]; then 66 | local ioption='-i .bak' 67 | else 68 | local ioption='-i' 69 | fi 70 | 71 | # set up testing suite if it doesn't yet exist 72 | if [ ! -d $WP_TESTS_DIR ]; then 73 | # set up testing suite 74 | mkdir -p $WP_TESTS_DIR 75 | svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes 76 | fi 77 | 78 | cd $WP_TESTS_DIR 79 | 80 | if [ ! -f wp-tests-config.php ]; then 81 | download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php 82 | sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR':" "$WP_TESTS_DIR"/wp-tests-config.php 83 | sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php 84 | sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php 85 | sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php 86 | sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php 87 | fi 88 | 89 | } 90 | 91 | install_db() { 92 | # parse DB_HOST for port or socket references 93 | local PARTS=(${DB_HOST//\:/ }) 94 | local DB_HOSTNAME=${PARTS[0]}; 95 | local DB_SOCK_OR_PORT=${PARTS[1]}; 96 | local EXTRA="" 97 | 98 | if ! [ -z $DB_HOSTNAME ] ; then 99 | if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then 100 | EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" 101 | elif ! [ -z $DB_SOCK_OR_PORT ] ; then 102 | EXTRA=" --socket=$DB_SOCK_OR_PORT" 103 | elif ! [ -z $DB_HOSTNAME ] ; then 104 | EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" 105 | fi 106 | fi 107 | 108 | # create database 109 | mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA 110 | } 111 | 112 | install_wp 113 | install_test_suite 114 | install_db -------------------------------------------------------------------------------- /bin/phpunit-no-cover.xml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | ../tests/ 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "upstatement/stream-manager", 3 | "type": "wordpress-plugin", 4 | "description": "Plugin to manage streams of content in WordPress", 5 | "keywords": [ 6 | "wordpress", 7 | "timber", 8 | "content management", 9 | "cms" 10 | ], 11 | "homepage": "http://github.com/upstatement/stream-manager", 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Jared Novack", 16 | "email": "jared@upstatement.com", 17 | "homepage": "http://upstatement.com" 18 | }, 19 | { 20 | "name": "Chris Voll", 21 | "email": "chirs.voll@upstatement.com", 22 | "homepage": "http://upstatement.com" 23 | }, 24 | { 25 | "name": "Gus Wrezerek", 26 | "email": "gus@upstatement.com", 27 | "homepage": "http://upstatement.com" 28 | }, 29 | { 30 | "name": "Rowan Krishnan", 31 | "email": "rowan@upstatement.com", 32 | "homepage": "http://upstatement.com" 33 | }, 34 | { 35 | "name": "Mike Swartz", 36 | "email": "mike@upstatement.com", 37 | "homepage": "http://upstatement.com" 38 | } 39 | ], 40 | "support": { 41 | "issues": "https://github.com/upstataement/stream-manager/issues", 42 | "wiki": "https://github.com/upstataement/stream-manager/wiki", 43 | "source": "https://github.com/upstataement/stream-manager" 44 | }, 45 | "require": { 46 | "php": ">=5.3.0" 47 | }, 48 | "require-dev": { 49 | "phpunit/phpunit": "5.7.*", 50 | "wpackagist-plugin/timber-library": "*", 51 | "composer/installers": "~1.0", 52 | "satooshi/php-coveralls": "dev-master" 53 | }, 54 | "repositories": [ 55 | { 56 | "type": "composer", 57 | "url": "http://wpackagist.org" 58 | } 59 | ], 60 | "config": { 61 | "secure-http": false 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "08e90ff5bd032f81abb3fdb54958af8c", 8 | "packages": [], 9 | "packages-dev": [ 10 | { 11 | "name": "composer/installers", 12 | "version": "v1.2.0", 13 | "source": { 14 | "type": "git", 15 | "url": "https://github.com/composer/installers.git", 16 | "reference": "d78064c68299743e0161004f2de3a0204e33b804" 17 | }, 18 | "dist": { 19 | "type": "zip", 20 | "url": "https://api.github.com/repos/composer/installers/zipball/d78064c68299743e0161004f2de3a0204e33b804", 21 | "reference": "d78064c68299743e0161004f2de3a0204e33b804", 22 | "shasum": "" 23 | }, 24 | "require": { 25 | "composer-plugin-api": "^1.0" 26 | }, 27 | "replace": { 28 | "roundcube/plugin-installer": "*", 29 | "shama/baton": "*" 30 | }, 31 | "require-dev": { 32 | "composer/composer": "1.0.*@dev", 33 | "phpunit/phpunit": "4.1.*" 34 | }, 35 | "type": "composer-plugin", 36 | "extra": { 37 | "class": "Composer\\Installers\\Plugin", 38 | "branch-alias": { 39 | "dev-master": "1.0-dev" 40 | } 41 | }, 42 | "autoload": { 43 | "psr-4": { 44 | "Composer\\Installers\\": "src/Composer/Installers" 45 | } 46 | }, 47 | "notification-url": "https://packagist.org/downloads/", 48 | "license": [ 49 | "MIT" 50 | ], 51 | "authors": [ 52 | { 53 | "name": "Kyle Robinson Young", 54 | "email": "kyle@dontkry.com", 55 | "homepage": "https://github.com/shama" 56 | } 57 | ], 58 | "description": "A multi-framework Composer library installer", 59 | "homepage": "https://composer.github.io/installers/", 60 | "keywords": [ 61 | "Craft", 62 | "Dolibarr", 63 | "Hurad", 64 | "ImageCMS", 65 | "MODX Evo", 66 | "Mautic", 67 | "OXID", 68 | "Plentymarkets", 69 | "RadPHP", 70 | "SMF", 71 | "Thelia", 72 | "WolfCMS", 73 | "agl", 74 | "aimeos", 75 | "annotatecms", 76 | "attogram", 77 | "bitrix", 78 | "cakephp", 79 | "chef", 80 | "cockpit", 81 | "codeigniter", 82 | "concrete5", 83 | "croogo", 84 | "dokuwiki", 85 | "drupal", 86 | "elgg", 87 | "expressionengine", 88 | "fuelphp", 89 | "grav", 90 | "installer", 91 | "joomla", 92 | "kohana", 93 | "laravel", 94 | "lithium", 95 | "magento", 96 | "mako", 97 | "mediawiki", 98 | "modulework", 99 | "moodle", 100 | "phpbb", 101 | "piwik", 102 | "ppi", 103 | "puppet", 104 | "reindex", 105 | "roundcube", 106 | "shopware", 107 | "silverstripe", 108 | "symfony", 109 | "typo3", 110 | "wordpress", 111 | "yawik", 112 | "zend", 113 | "zikula" 114 | ], 115 | "time": "2016-08-13T20:53:52+00:00" 116 | }, 117 | { 118 | "name": "doctrine/instantiator", 119 | "version": "1.0.5", 120 | "source": { 121 | "type": "git", 122 | "url": "https://github.com/doctrine/instantiator.git", 123 | "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" 124 | }, 125 | "dist": { 126 | "type": "zip", 127 | "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", 128 | "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", 129 | "shasum": "" 130 | }, 131 | "require": { 132 | "php": ">=5.3,<8.0-DEV" 133 | }, 134 | "require-dev": { 135 | "athletic/athletic": "~0.1.8", 136 | "ext-pdo": "*", 137 | "ext-phar": "*", 138 | "phpunit/phpunit": "~4.0", 139 | "squizlabs/php_codesniffer": "~2.0" 140 | }, 141 | "type": "library", 142 | "extra": { 143 | "branch-alias": { 144 | "dev-master": "1.0.x-dev" 145 | } 146 | }, 147 | "autoload": { 148 | "psr-4": { 149 | "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" 150 | } 151 | }, 152 | "notification-url": "https://packagist.org/downloads/", 153 | "license": [ 154 | "MIT" 155 | ], 156 | "authors": [ 157 | { 158 | "name": "Marco Pivetta", 159 | "email": "ocramius@gmail.com", 160 | "homepage": "http://ocramius.github.com/" 161 | } 162 | ], 163 | "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", 164 | "homepage": "https://github.com/doctrine/instantiator", 165 | "keywords": [ 166 | "constructor", 167 | "instantiate" 168 | ], 169 | "time": "2015-06-14T21:17:01+00:00" 170 | }, 171 | { 172 | "name": "guzzlehttp/guzzle", 173 | "version": "6.2.3", 174 | "source": { 175 | "type": "git", 176 | "url": "https://github.com/guzzle/guzzle.git", 177 | "reference": "8d6c6cc55186db87b7dc5009827429ba4e9dc006" 178 | }, 179 | "dist": { 180 | "type": "zip", 181 | "url": "https://api.github.com/repos/guzzle/guzzle/zipball/8d6c6cc55186db87b7dc5009827429ba4e9dc006", 182 | "reference": "8d6c6cc55186db87b7dc5009827429ba4e9dc006", 183 | "shasum": "" 184 | }, 185 | "require": { 186 | "guzzlehttp/promises": "^1.0", 187 | "guzzlehttp/psr7": "^1.4", 188 | "php": ">=5.5" 189 | }, 190 | "require-dev": { 191 | "ext-curl": "*", 192 | "phpunit/phpunit": "^4.0", 193 | "psr/log": "^1.0" 194 | }, 195 | "type": "library", 196 | "extra": { 197 | "branch-alias": { 198 | "dev-master": "6.2-dev" 199 | } 200 | }, 201 | "autoload": { 202 | "files": [ 203 | "src/functions_include.php" 204 | ], 205 | "psr-4": { 206 | "GuzzleHttp\\": "src/" 207 | } 208 | }, 209 | "notification-url": "https://packagist.org/downloads/", 210 | "license": [ 211 | "MIT" 212 | ], 213 | "authors": [ 214 | { 215 | "name": "Michael Dowling", 216 | "email": "mtdowling@gmail.com", 217 | "homepage": "https://github.com/mtdowling" 218 | } 219 | ], 220 | "description": "Guzzle is a PHP HTTP client library", 221 | "homepage": "http://guzzlephp.org/", 222 | "keywords": [ 223 | "client", 224 | "curl", 225 | "framework", 226 | "http", 227 | "http client", 228 | "rest", 229 | "web service" 230 | ], 231 | "time": "2017-02-28T22:50:30+00:00" 232 | }, 233 | { 234 | "name": "guzzlehttp/promises", 235 | "version": "v1.3.1", 236 | "source": { 237 | "type": "git", 238 | "url": "https://github.com/guzzle/promises.git", 239 | "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646" 240 | }, 241 | "dist": { 242 | "type": "zip", 243 | "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646", 244 | "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646", 245 | "shasum": "" 246 | }, 247 | "require": { 248 | "php": ">=5.5.0" 249 | }, 250 | "require-dev": { 251 | "phpunit/phpunit": "^4.0" 252 | }, 253 | "type": "library", 254 | "extra": { 255 | "branch-alias": { 256 | "dev-master": "1.4-dev" 257 | } 258 | }, 259 | "autoload": { 260 | "psr-4": { 261 | "GuzzleHttp\\Promise\\": "src/" 262 | }, 263 | "files": [ 264 | "src/functions_include.php" 265 | ] 266 | }, 267 | "notification-url": "https://packagist.org/downloads/", 268 | "license": [ 269 | "MIT" 270 | ], 271 | "authors": [ 272 | { 273 | "name": "Michael Dowling", 274 | "email": "mtdowling@gmail.com", 275 | "homepage": "https://github.com/mtdowling" 276 | } 277 | ], 278 | "description": "Guzzle promises library", 279 | "keywords": [ 280 | "promise" 281 | ], 282 | "time": "2016-12-20T10:07:11+00:00" 283 | }, 284 | { 285 | "name": "guzzlehttp/psr7", 286 | "version": "1.4.2", 287 | "source": { 288 | "type": "git", 289 | "url": "https://github.com/guzzle/psr7.git", 290 | "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c" 291 | }, 292 | "dist": { 293 | "type": "zip", 294 | "url": "https://api.github.com/repos/guzzle/psr7/zipball/f5b8a8512e2b58b0071a7280e39f14f72e05d87c", 295 | "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c", 296 | "shasum": "" 297 | }, 298 | "require": { 299 | "php": ">=5.4.0", 300 | "psr/http-message": "~1.0" 301 | }, 302 | "provide": { 303 | "psr/http-message-implementation": "1.0" 304 | }, 305 | "require-dev": { 306 | "phpunit/phpunit": "~4.0" 307 | }, 308 | "type": "library", 309 | "extra": { 310 | "branch-alias": { 311 | "dev-master": "1.4-dev" 312 | } 313 | }, 314 | "autoload": { 315 | "psr-4": { 316 | "GuzzleHttp\\Psr7\\": "src/" 317 | }, 318 | "files": [ 319 | "src/functions_include.php" 320 | ] 321 | }, 322 | "notification-url": "https://packagist.org/downloads/", 323 | "license": [ 324 | "MIT" 325 | ], 326 | "authors": [ 327 | { 328 | "name": "Michael Dowling", 329 | "email": "mtdowling@gmail.com", 330 | "homepage": "https://github.com/mtdowling" 331 | }, 332 | { 333 | "name": "Tobias Schultze", 334 | "homepage": "https://github.com/Tobion" 335 | } 336 | ], 337 | "description": "PSR-7 message implementation that also provides common utility methods", 338 | "keywords": [ 339 | "http", 340 | "message", 341 | "request", 342 | "response", 343 | "stream", 344 | "uri", 345 | "url" 346 | ], 347 | "time": "2017-03-20T17:10:46+00:00" 348 | }, 349 | { 350 | "name": "myclabs/deep-copy", 351 | "version": "1.6.0", 352 | "source": { 353 | "type": "git", 354 | "url": "https://github.com/myclabs/DeepCopy.git", 355 | "reference": "5a5a9fc8025a08d8919be87d6884d5a92520cefe" 356 | }, 357 | "dist": { 358 | "type": "zip", 359 | "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/5a5a9fc8025a08d8919be87d6884d5a92520cefe", 360 | "reference": "5a5a9fc8025a08d8919be87d6884d5a92520cefe", 361 | "shasum": "" 362 | }, 363 | "require": { 364 | "php": ">=5.4.0" 365 | }, 366 | "require-dev": { 367 | "doctrine/collections": "1.*", 368 | "phpunit/phpunit": "~4.1" 369 | }, 370 | "type": "library", 371 | "autoload": { 372 | "psr-4": { 373 | "DeepCopy\\": "src/DeepCopy/" 374 | } 375 | }, 376 | "notification-url": "https://packagist.org/downloads/", 377 | "license": [ 378 | "MIT" 379 | ], 380 | "description": "Create deep copies (clones) of your objects", 381 | "homepage": "https://github.com/myclabs/DeepCopy", 382 | "keywords": [ 383 | "clone", 384 | "copy", 385 | "duplicate", 386 | "object", 387 | "object graph" 388 | ], 389 | "time": "2017-01-26T22:05:40+00:00" 390 | }, 391 | { 392 | "name": "phpdocumentor/reflection-common", 393 | "version": "1.0", 394 | "source": { 395 | "type": "git", 396 | "url": "https://github.com/phpDocumentor/ReflectionCommon.git", 397 | "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c" 398 | }, 399 | "dist": { 400 | "type": "zip", 401 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/144c307535e82c8fdcaacbcfc1d6d8eeb896687c", 402 | "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c", 403 | "shasum": "" 404 | }, 405 | "require": { 406 | "php": ">=5.5" 407 | }, 408 | "require-dev": { 409 | "phpunit/phpunit": "^4.6" 410 | }, 411 | "type": "library", 412 | "extra": { 413 | "branch-alias": { 414 | "dev-master": "1.0.x-dev" 415 | } 416 | }, 417 | "autoload": { 418 | "psr-4": { 419 | "phpDocumentor\\Reflection\\": [ 420 | "src" 421 | ] 422 | } 423 | }, 424 | "notification-url": "https://packagist.org/downloads/", 425 | "license": [ 426 | "MIT" 427 | ], 428 | "authors": [ 429 | { 430 | "name": "Jaap van Otterdijk", 431 | "email": "opensource@ijaap.nl" 432 | } 433 | ], 434 | "description": "Common reflection classes used by phpdocumentor to reflect the code structure", 435 | "homepage": "http://www.phpdoc.org", 436 | "keywords": [ 437 | "FQSEN", 438 | "phpDocumentor", 439 | "phpdoc", 440 | "reflection", 441 | "static analysis" 442 | ], 443 | "time": "2015-12-27T11:43:31+00:00" 444 | }, 445 | { 446 | "name": "phpdocumentor/reflection-docblock", 447 | "version": "3.1.1", 448 | "source": { 449 | "type": "git", 450 | "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", 451 | "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e" 452 | }, 453 | "dist": { 454 | "type": "zip", 455 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/8331b5efe816ae05461b7ca1e721c01b46bafb3e", 456 | "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e", 457 | "shasum": "" 458 | }, 459 | "require": { 460 | "php": ">=5.5", 461 | "phpdocumentor/reflection-common": "^1.0@dev", 462 | "phpdocumentor/type-resolver": "^0.2.0", 463 | "webmozart/assert": "^1.0" 464 | }, 465 | "require-dev": { 466 | "mockery/mockery": "^0.9.4", 467 | "phpunit/phpunit": "^4.4" 468 | }, 469 | "type": "library", 470 | "autoload": { 471 | "psr-4": { 472 | "phpDocumentor\\Reflection\\": [ 473 | "src/" 474 | ] 475 | } 476 | }, 477 | "notification-url": "https://packagist.org/downloads/", 478 | "license": [ 479 | "MIT" 480 | ], 481 | "authors": [ 482 | { 483 | "name": "Mike van Riel", 484 | "email": "me@mikevanriel.com" 485 | } 486 | ], 487 | "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", 488 | "time": "2016-09-30T07:12:33+00:00" 489 | }, 490 | { 491 | "name": "phpdocumentor/type-resolver", 492 | "version": "0.2.1", 493 | "source": { 494 | "type": "git", 495 | "url": "https://github.com/phpDocumentor/TypeResolver.git", 496 | "reference": "e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb" 497 | }, 498 | "dist": { 499 | "type": "zip", 500 | "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb", 501 | "reference": "e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb", 502 | "shasum": "" 503 | }, 504 | "require": { 505 | "php": ">=5.5", 506 | "phpdocumentor/reflection-common": "^1.0" 507 | }, 508 | "require-dev": { 509 | "mockery/mockery": "^0.9.4", 510 | "phpunit/phpunit": "^5.2||^4.8.24" 511 | }, 512 | "type": "library", 513 | "extra": { 514 | "branch-alias": { 515 | "dev-master": "1.0.x-dev" 516 | } 517 | }, 518 | "autoload": { 519 | "psr-4": { 520 | "phpDocumentor\\Reflection\\": [ 521 | "src/" 522 | ] 523 | } 524 | }, 525 | "notification-url": "https://packagist.org/downloads/", 526 | "license": [ 527 | "MIT" 528 | ], 529 | "authors": [ 530 | { 531 | "name": "Mike van Riel", 532 | "email": "me@mikevanriel.com" 533 | } 534 | ], 535 | "time": "2016-11-25T06:54:22+00:00" 536 | }, 537 | { 538 | "name": "phpspec/prophecy", 539 | "version": "v1.7.0", 540 | "source": { 541 | "type": "git", 542 | "url": "https://github.com/phpspec/prophecy.git", 543 | "reference": "93d39f1f7f9326d746203c7c056f300f7f126073" 544 | }, 545 | "dist": { 546 | "type": "zip", 547 | "url": "https://api.github.com/repos/phpspec/prophecy/zipball/93d39f1f7f9326d746203c7c056f300f7f126073", 548 | "reference": "93d39f1f7f9326d746203c7c056f300f7f126073", 549 | "shasum": "" 550 | }, 551 | "require": { 552 | "doctrine/instantiator": "^1.0.2", 553 | "php": "^5.3|^7.0", 554 | "phpdocumentor/reflection-docblock": "^2.0|^3.0.2", 555 | "sebastian/comparator": "^1.1|^2.0", 556 | "sebastian/recursion-context": "^1.0|^2.0|^3.0" 557 | }, 558 | "require-dev": { 559 | "phpspec/phpspec": "^2.5|^3.2", 560 | "phpunit/phpunit": "^4.8 || ^5.6.5" 561 | }, 562 | "type": "library", 563 | "extra": { 564 | "branch-alias": { 565 | "dev-master": "1.6.x-dev" 566 | } 567 | }, 568 | "autoload": { 569 | "psr-0": { 570 | "Prophecy\\": "src/" 571 | } 572 | }, 573 | "notification-url": "https://packagist.org/downloads/", 574 | "license": [ 575 | "MIT" 576 | ], 577 | "authors": [ 578 | { 579 | "name": "Konstantin Kudryashov", 580 | "email": "ever.zet@gmail.com", 581 | "homepage": "http://everzet.com" 582 | }, 583 | { 584 | "name": "Marcello Duarte", 585 | "email": "marcello.duarte@gmail.com" 586 | } 587 | ], 588 | "description": "Highly opinionated mocking framework for PHP 5.3+", 589 | "homepage": "https://github.com/phpspec/prophecy", 590 | "keywords": [ 591 | "Double", 592 | "Dummy", 593 | "fake", 594 | "mock", 595 | "spy", 596 | "stub" 597 | ], 598 | "time": "2017-03-02T20:05:34+00:00" 599 | }, 600 | { 601 | "name": "phpunit/php-code-coverage", 602 | "version": "4.0.8", 603 | "source": { 604 | "type": "git", 605 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 606 | "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d" 607 | }, 608 | "dist": { 609 | "type": "zip", 610 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ef7b2f56815df854e66ceaee8ebe9393ae36a40d", 611 | "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d", 612 | "shasum": "" 613 | }, 614 | "require": { 615 | "ext-dom": "*", 616 | "ext-xmlwriter": "*", 617 | "php": "^5.6 || ^7.0", 618 | "phpunit/php-file-iterator": "^1.3", 619 | "phpunit/php-text-template": "^1.2", 620 | "phpunit/php-token-stream": "^1.4.2 || ^2.0", 621 | "sebastian/code-unit-reverse-lookup": "^1.0", 622 | "sebastian/environment": "^1.3.2 || ^2.0", 623 | "sebastian/version": "^1.0 || ^2.0" 624 | }, 625 | "require-dev": { 626 | "ext-xdebug": "^2.1.4", 627 | "phpunit/phpunit": "^5.7" 628 | }, 629 | "suggest": { 630 | "ext-xdebug": "^2.5.1" 631 | }, 632 | "type": "library", 633 | "extra": { 634 | "branch-alias": { 635 | "dev-master": "4.0.x-dev" 636 | } 637 | }, 638 | "autoload": { 639 | "classmap": [ 640 | "src/" 641 | ] 642 | }, 643 | "notification-url": "https://packagist.org/downloads/", 644 | "license": [ 645 | "BSD-3-Clause" 646 | ], 647 | "authors": [ 648 | { 649 | "name": "Sebastian Bergmann", 650 | "email": "sb@sebastian-bergmann.de", 651 | "role": "lead" 652 | } 653 | ], 654 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 655 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 656 | "keywords": [ 657 | "coverage", 658 | "testing", 659 | "xunit" 660 | ], 661 | "time": "2017-04-02T07:44:40+00:00" 662 | }, 663 | { 664 | "name": "phpunit/php-file-iterator", 665 | "version": "1.4.2", 666 | "source": { 667 | "type": "git", 668 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git", 669 | "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5" 670 | }, 671 | "dist": { 672 | "type": "zip", 673 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3cc8f69b3028d0f96a9078e6295d86e9bf019be5", 674 | "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5", 675 | "shasum": "" 676 | }, 677 | "require": { 678 | "php": ">=5.3.3" 679 | }, 680 | "type": "library", 681 | "extra": { 682 | "branch-alias": { 683 | "dev-master": "1.4.x-dev" 684 | } 685 | }, 686 | "autoload": { 687 | "classmap": [ 688 | "src/" 689 | ] 690 | }, 691 | "notification-url": "https://packagist.org/downloads/", 692 | "license": [ 693 | "BSD-3-Clause" 694 | ], 695 | "authors": [ 696 | { 697 | "name": "Sebastian Bergmann", 698 | "email": "sb@sebastian-bergmann.de", 699 | "role": "lead" 700 | } 701 | ], 702 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 703 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", 704 | "keywords": [ 705 | "filesystem", 706 | "iterator" 707 | ], 708 | "time": "2016-10-03T07:40:28+00:00" 709 | }, 710 | { 711 | "name": "phpunit/php-text-template", 712 | "version": "1.2.1", 713 | "source": { 714 | "type": "git", 715 | "url": "https://github.com/sebastianbergmann/php-text-template.git", 716 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" 717 | }, 718 | "dist": { 719 | "type": "zip", 720 | "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 721 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 722 | "shasum": "" 723 | }, 724 | "require": { 725 | "php": ">=5.3.3" 726 | }, 727 | "type": "library", 728 | "autoload": { 729 | "classmap": [ 730 | "src/" 731 | ] 732 | }, 733 | "notification-url": "https://packagist.org/downloads/", 734 | "license": [ 735 | "BSD-3-Clause" 736 | ], 737 | "authors": [ 738 | { 739 | "name": "Sebastian Bergmann", 740 | "email": "sebastian@phpunit.de", 741 | "role": "lead" 742 | } 743 | ], 744 | "description": "Simple template engine.", 745 | "homepage": "https://github.com/sebastianbergmann/php-text-template/", 746 | "keywords": [ 747 | "template" 748 | ], 749 | "time": "2015-06-21T13:50:34+00:00" 750 | }, 751 | { 752 | "name": "phpunit/php-timer", 753 | "version": "1.0.9", 754 | "source": { 755 | "type": "git", 756 | "url": "https://github.com/sebastianbergmann/php-timer.git", 757 | "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" 758 | }, 759 | "dist": { 760 | "type": "zip", 761 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", 762 | "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", 763 | "shasum": "" 764 | }, 765 | "require": { 766 | "php": "^5.3.3 || ^7.0" 767 | }, 768 | "require-dev": { 769 | "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" 770 | }, 771 | "type": "library", 772 | "extra": { 773 | "branch-alias": { 774 | "dev-master": "1.0-dev" 775 | } 776 | }, 777 | "autoload": { 778 | "classmap": [ 779 | "src/" 780 | ] 781 | }, 782 | "notification-url": "https://packagist.org/downloads/", 783 | "license": [ 784 | "BSD-3-Clause" 785 | ], 786 | "authors": [ 787 | { 788 | "name": "Sebastian Bergmann", 789 | "email": "sb@sebastian-bergmann.de", 790 | "role": "lead" 791 | } 792 | ], 793 | "description": "Utility class for timing", 794 | "homepage": "https://github.com/sebastianbergmann/php-timer/", 795 | "keywords": [ 796 | "timer" 797 | ], 798 | "time": "2017-02-26T11:10:40+00:00" 799 | }, 800 | { 801 | "name": "phpunit/php-token-stream", 802 | "version": "1.4.11", 803 | "source": { 804 | "type": "git", 805 | "url": "https://github.com/sebastianbergmann/php-token-stream.git", 806 | "reference": "e03f8f67534427a787e21a385a67ec3ca6978ea7" 807 | }, 808 | "dist": { 809 | "type": "zip", 810 | "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/e03f8f67534427a787e21a385a67ec3ca6978ea7", 811 | "reference": "e03f8f67534427a787e21a385a67ec3ca6978ea7", 812 | "shasum": "" 813 | }, 814 | "require": { 815 | "ext-tokenizer": "*", 816 | "php": ">=5.3.3" 817 | }, 818 | "require-dev": { 819 | "phpunit/phpunit": "~4.2" 820 | }, 821 | "type": "library", 822 | "extra": { 823 | "branch-alias": { 824 | "dev-master": "1.4-dev" 825 | } 826 | }, 827 | "autoload": { 828 | "classmap": [ 829 | "src/" 830 | ] 831 | }, 832 | "notification-url": "https://packagist.org/downloads/", 833 | "license": [ 834 | "BSD-3-Clause" 835 | ], 836 | "authors": [ 837 | { 838 | "name": "Sebastian Bergmann", 839 | "email": "sebastian@phpunit.de" 840 | } 841 | ], 842 | "description": "Wrapper around PHP's tokenizer extension.", 843 | "homepage": "https://github.com/sebastianbergmann/php-token-stream/", 844 | "keywords": [ 845 | "tokenizer" 846 | ], 847 | "time": "2017-02-27T10:12:30+00:00" 848 | }, 849 | { 850 | "name": "phpunit/phpunit", 851 | "version": "5.7.13", 852 | "source": { 853 | "type": "git", 854 | "url": "https://github.com/sebastianbergmann/phpunit.git", 855 | "reference": "60ebeed87a35ea46fd7f7d8029df2d6f013ebb34" 856 | }, 857 | "dist": { 858 | "type": "zip", 859 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/60ebeed87a35ea46fd7f7d8029df2d6f013ebb34", 860 | "reference": "60ebeed87a35ea46fd7f7d8029df2d6f013ebb34", 861 | "shasum": "" 862 | }, 863 | "require": { 864 | "ext-dom": "*", 865 | "ext-json": "*", 866 | "ext-libxml": "*", 867 | "ext-mbstring": "*", 868 | "ext-xml": "*", 869 | "myclabs/deep-copy": "~1.3", 870 | "php": "^5.6 || ^7.0", 871 | "phpspec/prophecy": "^1.6.2", 872 | "phpunit/php-code-coverage": "^4.0.4", 873 | "phpunit/php-file-iterator": "~1.4", 874 | "phpunit/php-text-template": "~1.2", 875 | "phpunit/php-timer": "^1.0.6", 876 | "phpunit/phpunit-mock-objects": "^3.2", 877 | "sebastian/comparator": "^1.2.4", 878 | "sebastian/diff": "~1.2", 879 | "sebastian/environment": "^1.3.4 || ^2.0", 880 | "sebastian/exporter": "~2.0", 881 | "sebastian/global-state": "^1.1", 882 | "sebastian/object-enumerator": "~2.0", 883 | "sebastian/resource-operations": "~1.0", 884 | "sebastian/version": "~1.0|~2.0", 885 | "symfony/yaml": "~2.1|~3.0" 886 | }, 887 | "conflict": { 888 | "phpdocumentor/reflection-docblock": "3.0.2" 889 | }, 890 | "require-dev": { 891 | "ext-pdo": "*" 892 | }, 893 | "suggest": { 894 | "ext-xdebug": "*", 895 | "phpunit/php-invoker": "~1.1" 896 | }, 897 | "bin": [ 898 | "phpunit" 899 | ], 900 | "type": "library", 901 | "extra": { 902 | "branch-alias": { 903 | "dev-master": "5.7.x-dev" 904 | } 905 | }, 906 | "autoload": { 907 | "classmap": [ 908 | "src/" 909 | ] 910 | }, 911 | "notification-url": "https://packagist.org/downloads/", 912 | "license": [ 913 | "BSD-3-Clause" 914 | ], 915 | "authors": [ 916 | { 917 | "name": "Sebastian Bergmann", 918 | "email": "sebastian@phpunit.de", 919 | "role": "lead" 920 | } 921 | ], 922 | "description": "The PHP Unit Testing framework.", 923 | "homepage": "https://phpunit.de/", 924 | "keywords": [ 925 | "phpunit", 926 | "testing", 927 | "xunit" 928 | ], 929 | "time": "2017-02-10T09:05:10+00:00" 930 | }, 931 | { 932 | "name": "phpunit/phpunit-mock-objects", 933 | "version": "3.4.3", 934 | "source": { 935 | "type": "git", 936 | "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", 937 | "reference": "3ab72b65b39b491e0c011e2e09bb2206c2aa8e24" 938 | }, 939 | "dist": { 940 | "type": "zip", 941 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/3ab72b65b39b491e0c011e2e09bb2206c2aa8e24", 942 | "reference": "3ab72b65b39b491e0c011e2e09bb2206c2aa8e24", 943 | "shasum": "" 944 | }, 945 | "require": { 946 | "doctrine/instantiator": "^1.0.2", 947 | "php": "^5.6 || ^7.0", 948 | "phpunit/php-text-template": "^1.2", 949 | "sebastian/exporter": "^1.2 || ^2.0" 950 | }, 951 | "conflict": { 952 | "phpunit/phpunit": "<5.4.0" 953 | }, 954 | "require-dev": { 955 | "phpunit/phpunit": "^5.4" 956 | }, 957 | "suggest": { 958 | "ext-soap": "*" 959 | }, 960 | "type": "library", 961 | "extra": { 962 | "branch-alias": { 963 | "dev-master": "3.2.x-dev" 964 | } 965 | }, 966 | "autoload": { 967 | "classmap": [ 968 | "src/" 969 | ] 970 | }, 971 | "notification-url": "https://packagist.org/downloads/", 972 | "license": [ 973 | "BSD-3-Clause" 974 | ], 975 | "authors": [ 976 | { 977 | "name": "Sebastian Bergmann", 978 | "email": "sb@sebastian-bergmann.de", 979 | "role": "lead" 980 | } 981 | ], 982 | "description": "Mock Object library for PHPUnit", 983 | "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", 984 | "keywords": [ 985 | "mock", 986 | "xunit" 987 | ], 988 | "time": "2016-12-08T20:27:08+00:00" 989 | }, 990 | { 991 | "name": "psr/http-message", 992 | "version": "1.0.1", 993 | "source": { 994 | "type": "git", 995 | "url": "https://github.com/php-fig/http-message.git", 996 | "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" 997 | }, 998 | "dist": { 999 | "type": "zip", 1000 | "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", 1001 | "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", 1002 | "shasum": "" 1003 | }, 1004 | "require": { 1005 | "php": ">=5.3.0" 1006 | }, 1007 | "type": "library", 1008 | "extra": { 1009 | "branch-alias": { 1010 | "dev-master": "1.0.x-dev" 1011 | } 1012 | }, 1013 | "autoload": { 1014 | "psr-4": { 1015 | "Psr\\Http\\Message\\": "src/" 1016 | } 1017 | }, 1018 | "notification-url": "https://packagist.org/downloads/", 1019 | "license": [ 1020 | "MIT" 1021 | ], 1022 | "authors": [ 1023 | { 1024 | "name": "PHP-FIG", 1025 | "homepage": "http://www.php-fig.org/" 1026 | } 1027 | ], 1028 | "description": "Common interface for HTTP messages", 1029 | "homepage": "https://github.com/php-fig/http-message", 1030 | "keywords": [ 1031 | "http", 1032 | "http-message", 1033 | "psr", 1034 | "psr-7", 1035 | "request", 1036 | "response" 1037 | ], 1038 | "time": "2016-08-06T14:39:51+00:00" 1039 | }, 1040 | { 1041 | "name": "psr/log", 1042 | "version": "1.0.2", 1043 | "source": { 1044 | "type": "git", 1045 | "url": "https://github.com/php-fig/log.git", 1046 | "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" 1047 | }, 1048 | "dist": { 1049 | "type": "zip", 1050 | "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", 1051 | "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", 1052 | "shasum": "" 1053 | }, 1054 | "require": { 1055 | "php": ">=5.3.0" 1056 | }, 1057 | "type": "library", 1058 | "extra": { 1059 | "branch-alias": { 1060 | "dev-master": "1.0.x-dev" 1061 | } 1062 | }, 1063 | "autoload": { 1064 | "psr-4": { 1065 | "Psr\\Log\\": "Psr/Log/" 1066 | } 1067 | }, 1068 | "notification-url": "https://packagist.org/downloads/", 1069 | "license": [ 1070 | "MIT" 1071 | ], 1072 | "authors": [ 1073 | { 1074 | "name": "PHP-FIG", 1075 | "homepage": "http://www.php-fig.org/" 1076 | } 1077 | ], 1078 | "description": "Common interface for logging libraries", 1079 | "homepage": "https://github.com/php-fig/log", 1080 | "keywords": [ 1081 | "log", 1082 | "psr", 1083 | "psr-3" 1084 | ], 1085 | "time": "2016-10-10T12:19:37+00:00" 1086 | }, 1087 | { 1088 | "name": "satooshi/php-coveralls", 1089 | "version": "dev-master", 1090 | "source": { 1091 | "type": "git", 1092 | "url": "https://github.com/satooshi/php-coveralls.git", 1093 | "reference": "d7285decc88dff59c5ff02c4b1052ab424ba7fa5" 1094 | }, 1095 | "dist": { 1096 | "type": "zip", 1097 | "url": "https://api.github.com/repos/satooshi/php-coveralls/zipball/d7285decc88dff59c5ff02c4b1052ab424ba7fa5", 1098 | "reference": "d7285decc88dff59c5ff02c4b1052ab424ba7fa5", 1099 | "shasum": "" 1100 | }, 1101 | "require": { 1102 | "ext-json": "*", 1103 | "ext-simplexml": "*", 1104 | "guzzlehttp/guzzle": "^6.0", 1105 | "php": "^5.5 || ^7.0", 1106 | "psr/log": "^1.0", 1107 | "symfony/config": "^2.1 || ^3.0", 1108 | "symfony/console": "^2.1 || ^3.0", 1109 | "symfony/stopwatch": "^2.0 || ^3.0", 1110 | "symfony/yaml": "^2.0 || ^3.0" 1111 | }, 1112 | "suggest": { 1113 | "symfony/http-kernel": "Allows Symfony integration" 1114 | }, 1115 | "bin": [ 1116 | "bin/coveralls" 1117 | ], 1118 | "type": "library", 1119 | "extra": { 1120 | "branch-alias": { 1121 | "dev-master": "2.0-dev" 1122 | } 1123 | }, 1124 | "autoload": { 1125 | "psr-4": { 1126 | "Satooshi\\": "src/Satooshi/" 1127 | } 1128 | }, 1129 | "notification-url": "https://packagist.org/downloads/", 1130 | "license": [ 1131 | "MIT" 1132 | ], 1133 | "authors": [ 1134 | { 1135 | "name": "Kitamura Satoshi", 1136 | "email": "with.no.parachute@gmail.com", 1137 | "homepage": "https://www.facebook.com/satooshi.jp" 1138 | } 1139 | ], 1140 | "description": "PHP client library for Coveralls API", 1141 | "homepage": "https://github.com/satooshi/php-coveralls", 1142 | "keywords": [ 1143 | "ci", 1144 | "coverage", 1145 | "github", 1146 | "test" 1147 | ], 1148 | "time": "2017-03-31 10:12:47" 1149 | }, 1150 | { 1151 | "name": "sebastian/code-unit-reverse-lookup", 1152 | "version": "1.0.1", 1153 | "source": { 1154 | "type": "git", 1155 | "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", 1156 | "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" 1157 | }, 1158 | "dist": { 1159 | "type": "zip", 1160 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", 1161 | "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", 1162 | "shasum": "" 1163 | }, 1164 | "require": { 1165 | "php": "^5.6 || ^7.0" 1166 | }, 1167 | "require-dev": { 1168 | "phpunit/phpunit": "^5.7 || ^6.0" 1169 | }, 1170 | "type": "library", 1171 | "extra": { 1172 | "branch-alias": { 1173 | "dev-master": "1.0.x-dev" 1174 | } 1175 | }, 1176 | "autoload": { 1177 | "classmap": [ 1178 | "src/" 1179 | ] 1180 | }, 1181 | "notification-url": "https://packagist.org/downloads/", 1182 | "license": [ 1183 | "BSD-3-Clause" 1184 | ], 1185 | "authors": [ 1186 | { 1187 | "name": "Sebastian Bergmann", 1188 | "email": "sebastian@phpunit.de" 1189 | } 1190 | ], 1191 | "description": "Looks up which function or method a line of code belongs to", 1192 | "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", 1193 | "time": "2017-03-04T06:30:41+00:00" 1194 | }, 1195 | { 1196 | "name": "sebastian/comparator", 1197 | "version": "1.2.4", 1198 | "source": { 1199 | "type": "git", 1200 | "url": "https://github.com/sebastianbergmann/comparator.git", 1201 | "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be" 1202 | }, 1203 | "dist": { 1204 | "type": "zip", 1205 | "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", 1206 | "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", 1207 | "shasum": "" 1208 | }, 1209 | "require": { 1210 | "php": ">=5.3.3", 1211 | "sebastian/diff": "~1.2", 1212 | "sebastian/exporter": "~1.2 || ~2.0" 1213 | }, 1214 | "require-dev": { 1215 | "phpunit/phpunit": "~4.4" 1216 | }, 1217 | "type": "library", 1218 | "extra": { 1219 | "branch-alias": { 1220 | "dev-master": "1.2.x-dev" 1221 | } 1222 | }, 1223 | "autoload": { 1224 | "classmap": [ 1225 | "src/" 1226 | ] 1227 | }, 1228 | "notification-url": "https://packagist.org/downloads/", 1229 | "license": [ 1230 | "BSD-3-Clause" 1231 | ], 1232 | "authors": [ 1233 | { 1234 | "name": "Jeff Welch", 1235 | "email": "whatthejeff@gmail.com" 1236 | }, 1237 | { 1238 | "name": "Volker Dusch", 1239 | "email": "github@wallbash.com" 1240 | }, 1241 | { 1242 | "name": "Bernhard Schussek", 1243 | "email": "bschussek@2bepublished.at" 1244 | }, 1245 | { 1246 | "name": "Sebastian Bergmann", 1247 | "email": "sebastian@phpunit.de" 1248 | } 1249 | ], 1250 | "description": "Provides the functionality to compare PHP values for equality", 1251 | "homepage": "http://www.github.com/sebastianbergmann/comparator", 1252 | "keywords": [ 1253 | "comparator", 1254 | "compare", 1255 | "equality" 1256 | ], 1257 | "time": "2017-01-29T09:50:25+00:00" 1258 | }, 1259 | { 1260 | "name": "sebastian/diff", 1261 | "version": "1.4.1", 1262 | "source": { 1263 | "type": "git", 1264 | "url": "https://github.com/sebastianbergmann/diff.git", 1265 | "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e" 1266 | }, 1267 | "dist": { 1268 | "type": "zip", 1269 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e", 1270 | "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e", 1271 | "shasum": "" 1272 | }, 1273 | "require": { 1274 | "php": ">=5.3.3" 1275 | }, 1276 | "require-dev": { 1277 | "phpunit/phpunit": "~4.8" 1278 | }, 1279 | "type": "library", 1280 | "extra": { 1281 | "branch-alias": { 1282 | "dev-master": "1.4-dev" 1283 | } 1284 | }, 1285 | "autoload": { 1286 | "classmap": [ 1287 | "src/" 1288 | ] 1289 | }, 1290 | "notification-url": "https://packagist.org/downloads/", 1291 | "license": [ 1292 | "BSD-3-Clause" 1293 | ], 1294 | "authors": [ 1295 | { 1296 | "name": "Kore Nordmann", 1297 | "email": "mail@kore-nordmann.de" 1298 | }, 1299 | { 1300 | "name": "Sebastian Bergmann", 1301 | "email": "sebastian@phpunit.de" 1302 | } 1303 | ], 1304 | "description": "Diff implementation", 1305 | "homepage": "https://github.com/sebastianbergmann/diff", 1306 | "keywords": [ 1307 | "diff" 1308 | ], 1309 | "time": "2015-12-08T07:14:41+00:00" 1310 | }, 1311 | { 1312 | "name": "sebastian/environment", 1313 | "version": "2.0.0", 1314 | "source": { 1315 | "type": "git", 1316 | "url": "https://github.com/sebastianbergmann/environment.git", 1317 | "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac" 1318 | }, 1319 | "dist": { 1320 | "type": "zip", 1321 | "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/5795ffe5dc5b02460c3e34222fee8cbe245d8fac", 1322 | "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac", 1323 | "shasum": "" 1324 | }, 1325 | "require": { 1326 | "php": "^5.6 || ^7.0" 1327 | }, 1328 | "require-dev": { 1329 | "phpunit/phpunit": "^5.0" 1330 | }, 1331 | "type": "library", 1332 | "extra": { 1333 | "branch-alias": { 1334 | "dev-master": "2.0.x-dev" 1335 | } 1336 | }, 1337 | "autoload": { 1338 | "classmap": [ 1339 | "src/" 1340 | ] 1341 | }, 1342 | "notification-url": "https://packagist.org/downloads/", 1343 | "license": [ 1344 | "BSD-3-Clause" 1345 | ], 1346 | "authors": [ 1347 | { 1348 | "name": "Sebastian Bergmann", 1349 | "email": "sebastian@phpunit.de" 1350 | } 1351 | ], 1352 | "description": "Provides functionality to handle HHVM/PHP environments", 1353 | "homepage": "http://www.github.com/sebastianbergmann/environment", 1354 | "keywords": [ 1355 | "Xdebug", 1356 | "environment", 1357 | "hhvm" 1358 | ], 1359 | "time": "2016-11-26T07:53:53+00:00" 1360 | }, 1361 | { 1362 | "name": "sebastian/exporter", 1363 | "version": "2.0.0", 1364 | "source": { 1365 | "type": "git", 1366 | "url": "https://github.com/sebastianbergmann/exporter.git", 1367 | "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4" 1368 | }, 1369 | "dist": { 1370 | "type": "zip", 1371 | "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4", 1372 | "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4", 1373 | "shasum": "" 1374 | }, 1375 | "require": { 1376 | "php": ">=5.3.3", 1377 | "sebastian/recursion-context": "~2.0" 1378 | }, 1379 | "require-dev": { 1380 | "ext-mbstring": "*", 1381 | "phpunit/phpunit": "~4.4" 1382 | }, 1383 | "type": "library", 1384 | "extra": { 1385 | "branch-alias": { 1386 | "dev-master": "2.0.x-dev" 1387 | } 1388 | }, 1389 | "autoload": { 1390 | "classmap": [ 1391 | "src/" 1392 | ] 1393 | }, 1394 | "notification-url": "https://packagist.org/downloads/", 1395 | "license": [ 1396 | "BSD-3-Clause" 1397 | ], 1398 | "authors": [ 1399 | { 1400 | "name": "Jeff Welch", 1401 | "email": "whatthejeff@gmail.com" 1402 | }, 1403 | { 1404 | "name": "Volker Dusch", 1405 | "email": "github@wallbash.com" 1406 | }, 1407 | { 1408 | "name": "Bernhard Schussek", 1409 | "email": "bschussek@2bepublished.at" 1410 | }, 1411 | { 1412 | "name": "Sebastian Bergmann", 1413 | "email": "sebastian@phpunit.de" 1414 | }, 1415 | { 1416 | "name": "Adam Harvey", 1417 | "email": "aharvey@php.net" 1418 | } 1419 | ], 1420 | "description": "Provides the functionality to export PHP variables for visualization", 1421 | "homepage": "http://www.github.com/sebastianbergmann/exporter", 1422 | "keywords": [ 1423 | "export", 1424 | "exporter" 1425 | ], 1426 | "time": "2016-11-19T08:54:04+00:00" 1427 | }, 1428 | { 1429 | "name": "sebastian/global-state", 1430 | "version": "1.1.1", 1431 | "source": { 1432 | "type": "git", 1433 | "url": "https://github.com/sebastianbergmann/global-state.git", 1434 | "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" 1435 | }, 1436 | "dist": { 1437 | "type": "zip", 1438 | "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", 1439 | "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", 1440 | "shasum": "" 1441 | }, 1442 | "require": { 1443 | "php": ">=5.3.3" 1444 | }, 1445 | "require-dev": { 1446 | "phpunit/phpunit": "~4.2" 1447 | }, 1448 | "suggest": { 1449 | "ext-uopz": "*" 1450 | }, 1451 | "type": "library", 1452 | "extra": { 1453 | "branch-alias": { 1454 | "dev-master": "1.0-dev" 1455 | } 1456 | }, 1457 | "autoload": { 1458 | "classmap": [ 1459 | "src/" 1460 | ] 1461 | }, 1462 | "notification-url": "https://packagist.org/downloads/", 1463 | "license": [ 1464 | "BSD-3-Clause" 1465 | ], 1466 | "authors": [ 1467 | { 1468 | "name": "Sebastian Bergmann", 1469 | "email": "sebastian@phpunit.de" 1470 | } 1471 | ], 1472 | "description": "Snapshotting of global state", 1473 | "homepage": "http://www.github.com/sebastianbergmann/global-state", 1474 | "keywords": [ 1475 | "global state" 1476 | ], 1477 | "time": "2015-10-12T03:26:01+00:00" 1478 | }, 1479 | { 1480 | "name": "sebastian/object-enumerator", 1481 | "version": "2.0.1", 1482 | "source": { 1483 | "type": "git", 1484 | "url": "https://github.com/sebastianbergmann/object-enumerator.git", 1485 | "reference": "1311872ac850040a79c3c058bea3e22d0f09cbb7" 1486 | }, 1487 | "dist": { 1488 | "type": "zip", 1489 | "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1311872ac850040a79c3c058bea3e22d0f09cbb7", 1490 | "reference": "1311872ac850040a79c3c058bea3e22d0f09cbb7", 1491 | "shasum": "" 1492 | }, 1493 | "require": { 1494 | "php": ">=5.6", 1495 | "sebastian/recursion-context": "~2.0" 1496 | }, 1497 | "require-dev": { 1498 | "phpunit/phpunit": "~5" 1499 | }, 1500 | "type": "library", 1501 | "extra": { 1502 | "branch-alias": { 1503 | "dev-master": "2.0.x-dev" 1504 | } 1505 | }, 1506 | "autoload": { 1507 | "classmap": [ 1508 | "src/" 1509 | ] 1510 | }, 1511 | "notification-url": "https://packagist.org/downloads/", 1512 | "license": [ 1513 | "BSD-3-Clause" 1514 | ], 1515 | "authors": [ 1516 | { 1517 | "name": "Sebastian Bergmann", 1518 | "email": "sebastian@phpunit.de" 1519 | } 1520 | ], 1521 | "description": "Traverses array structures and object graphs to enumerate all referenced objects", 1522 | "homepage": "https://github.com/sebastianbergmann/object-enumerator/", 1523 | "time": "2017-02-18T15:18:39+00:00" 1524 | }, 1525 | { 1526 | "name": "sebastian/recursion-context", 1527 | "version": "2.0.0", 1528 | "source": { 1529 | "type": "git", 1530 | "url": "https://github.com/sebastianbergmann/recursion-context.git", 1531 | "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a" 1532 | }, 1533 | "dist": { 1534 | "type": "zip", 1535 | "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/2c3ba150cbec723aa057506e73a8d33bdb286c9a", 1536 | "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a", 1537 | "shasum": "" 1538 | }, 1539 | "require": { 1540 | "php": ">=5.3.3" 1541 | }, 1542 | "require-dev": { 1543 | "phpunit/phpunit": "~4.4" 1544 | }, 1545 | "type": "library", 1546 | "extra": { 1547 | "branch-alias": { 1548 | "dev-master": "2.0.x-dev" 1549 | } 1550 | }, 1551 | "autoload": { 1552 | "classmap": [ 1553 | "src/" 1554 | ] 1555 | }, 1556 | "notification-url": "https://packagist.org/downloads/", 1557 | "license": [ 1558 | "BSD-3-Clause" 1559 | ], 1560 | "authors": [ 1561 | { 1562 | "name": "Jeff Welch", 1563 | "email": "whatthejeff@gmail.com" 1564 | }, 1565 | { 1566 | "name": "Sebastian Bergmann", 1567 | "email": "sebastian@phpunit.de" 1568 | }, 1569 | { 1570 | "name": "Adam Harvey", 1571 | "email": "aharvey@php.net" 1572 | } 1573 | ], 1574 | "description": "Provides functionality to recursively process PHP variables", 1575 | "homepage": "http://www.github.com/sebastianbergmann/recursion-context", 1576 | "time": "2016-11-19T07:33:16+00:00" 1577 | }, 1578 | { 1579 | "name": "sebastian/resource-operations", 1580 | "version": "1.0.0", 1581 | "source": { 1582 | "type": "git", 1583 | "url": "https://github.com/sebastianbergmann/resource-operations.git", 1584 | "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" 1585 | }, 1586 | "dist": { 1587 | "type": "zip", 1588 | "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", 1589 | "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", 1590 | "shasum": "" 1591 | }, 1592 | "require": { 1593 | "php": ">=5.6.0" 1594 | }, 1595 | "type": "library", 1596 | "extra": { 1597 | "branch-alias": { 1598 | "dev-master": "1.0.x-dev" 1599 | } 1600 | }, 1601 | "autoload": { 1602 | "classmap": [ 1603 | "src/" 1604 | ] 1605 | }, 1606 | "notification-url": "https://packagist.org/downloads/", 1607 | "license": [ 1608 | "BSD-3-Clause" 1609 | ], 1610 | "authors": [ 1611 | { 1612 | "name": "Sebastian Bergmann", 1613 | "email": "sebastian@phpunit.de" 1614 | } 1615 | ], 1616 | "description": "Provides a list of PHP built-in functions that operate on resources", 1617 | "homepage": "https://www.github.com/sebastianbergmann/resource-operations", 1618 | "time": "2015-07-28T20:34:47+00:00" 1619 | }, 1620 | { 1621 | "name": "sebastian/version", 1622 | "version": "2.0.1", 1623 | "source": { 1624 | "type": "git", 1625 | "url": "https://github.com/sebastianbergmann/version.git", 1626 | "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" 1627 | }, 1628 | "dist": { 1629 | "type": "zip", 1630 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", 1631 | "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", 1632 | "shasum": "" 1633 | }, 1634 | "require": { 1635 | "php": ">=5.6" 1636 | }, 1637 | "type": "library", 1638 | "extra": { 1639 | "branch-alias": { 1640 | "dev-master": "2.0.x-dev" 1641 | } 1642 | }, 1643 | "autoload": { 1644 | "classmap": [ 1645 | "src/" 1646 | ] 1647 | }, 1648 | "notification-url": "https://packagist.org/downloads/", 1649 | "license": [ 1650 | "BSD-3-Clause" 1651 | ], 1652 | "authors": [ 1653 | { 1654 | "name": "Sebastian Bergmann", 1655 | "email": "sebastian@phpunit.de", 1656 | "role": "lead" 1657 | } 1658 | ], 1659 | "description": "Library that helps with managing the version number of Git-hosted PHP projects", 1660 | "homepage": "https://github.com/sebastianbergmann/version", 1661 | "time": "2016-10-03T07:35:21+00:00" 1662 | }, 1663 | { 1664 | "name": "symfony/config", 1665 | "version": "v3.2.7", 1666 | "source": { 1667 | "type": "git", 1668 | "url": "https://github.com/symfony/config.git", 1669 | "reference": "8444bde28e3c2a33e571e6f180c2d78bfdc4480d" 1670 | }, 1671 | "dist": { 1672 | "type": "zip", 1673 | "url": "https://api.github.com/repos/symfony/config/zipball/8444bde28e3c2a33e571e6f180c2d78bfdc4480d", 1674 | "reference": "8444bde28e3c2a33e571e6f180c2d78bfdc4480d", 1675 | "shasum": "" 1676 | }, 1677 | "require": { 1678 | "php": ">=5.5.9", 1679 | "symfony/filesystem": "~2.8|~3.0" 1680 | }, 1681 | "require-dev": { 1682 | "symfony/yaml": "~3.0" 1683 | }, 1684 | "suggest": { 1685 | "symfony/yaml": "To use the yaml reference dumper" 1686 | }, 1687 | "type": "library", 1688 | "extra": { 1689 | "branch-alias": { 1690 | "dev-master": "3.2-dev" 1691 | } 1692 | }, 1693 | "autoload": { 1694 | "psr-4": { 1695 | "Symfony\\Component\\Config\\": "" 1696 | }, 1697 | "exclude-from-classmap": [ 1698 | "/Tests/" 1699 | ] 1700 | }, 1701 | "notification-url": "https://packagist.org/downloads/", 1702 | "license": [ 1703 | "MIT" 1704 | ], 1705 | "authors": [ 1706 | { 1707 | "name": "Fabien Potencier", 1708 | "email": "fabien@symfony.com" 1709 | }, 1710 | { 1711 | "name": "Symfony Community", 1712 | "homepage": "https://symfony.com/contributors" 1713 | } 1714 | ], 1715 | "description": "Symfony Config Component", 1716 | "homepage": "https://symfony.com", 1717 | "time": "2017-04-04T15:30:56+00:00" 1718 | }, 1719 | { 1720 | "name": "symfony/console", 1721 | "version": "v3.2.7", 1722 | "source": { 1723 | "type": "git", 1724 | "url": "https://github.com/symfony/console.git", 1725 | "reference": "c30243cc51f726812be3551316b109a2f5deaf8d" 1726 | }, 1727 | "dist": { 1728 | "type": "zip", 1729 | "url": "https://api.github.com/repos/symfony/console/zipball/c30243cc51f726812be3551316b109a2f5deaf8d", 1730 | "reference": "c30243cc51f726812be3551316b109a2f5deaf8d", 1731 | "shasum": "" 1732 | }, 1733 | "require": { 1734 | "php": ">=5.5.9", 1735 | "symfony/debug": "~2.8|~3.0", 1736 | "symfony/polyfill-mbstring": "~1.0" 1737 | }, 1738 | "require-dev": { 1739 | "psr/log": "~1.0", 1740 | "symfony/event-dispatcher": "~2.8|~3.0", 1741 | "symfony/filesystem": "~2.8|~3.0", 1742 | "symfony/process": "~2.8|~3.0" 1743 | }, 1744 | "suggest": { 1745 | "psr/log": "For using the console logger", 1746 | "symfony/event-dispatcher": "", 1747 | "symfony/filesystem": "", 1748 | "symfony/process": "" 1749 | }, 1750 | "type": "library", 1751 | "extra": { 1752 | "branch-alias": { 1753 | "dev-master": "3.2-dev" 1754 | } 1755 | }, 1756 | "autoload": { 1757 | "psr-4": { 1758 | "Symfony\\Component\\Console\\": "" 1759 | }, 1760 | "exclude-from-classmap": [ 1761 | "/Tests/" 1762 | ] 1763 | }, 1764 | "notification-url": "https://packagist.org/downloads/", 1765 | "license": [ 1766 | "MIT" 1767 | ], 1768 | "authors": [ 1769 | { 1770 | "name": "Fabien Potencier", 1771 | "email": "fabien@symfony.com" 1772 | }, 1773 | { 1774 | "name": "Symfony Community", 1775 | "homepage": "https://symfony.com/contributors" 1776 | } 1777 | ], 1778 | "description": "Symfony Console Component", 1779 | "homepage": "https://symfony.com", 1780 | "time": "2017-04-04T14:33:42+00:00" 1781 | }, 1782 | { 1783 | "name": "symfony/debug", 1784 | "version": "v3.2.7", 1785 | "source": { 1786 | "type": "git", 1787 | "url": "https://github.com/symfony/debug.git", 1788 | "reference": "56f613406446a4a0a031475cfd0a01751de22659" 1789 | }, 1790 | "dist": { 1791 | "type": "zip", 1792 | "url": "https://api.github.com/repos/symfony/debug/zipball/56f613406446a4a0a031475cfd0a01751de22659", 1793 | "reference": "56f613406446a4a0a031475cfd0a01751de22659", 1794 | "shasum": "" 1795 | }, 1796 | "require": { 1797 | "php": ">=5.5.9", 1798 | "psr/log": "~1.0" 1799 | }, 1800 | "conflict": { 1801 | "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2" 1802 | }, 1803 | "require-dev": { 1804 | "symfony/class-loader": "~2.8|~3.0", 1805 | "symfony/http-kernel": "~2.8|~3.0" 1806 | }, 1807 | "type": "library", 1808 | "extra": { 1809 | "branch-alias": { 1810 | "dev-master": "3.2-dev" 1811 | } 1812 | }, 1813 | "autoload": { 1814 | "psr-4": { 1815 | "Symfony\\Component\\Debug\\": "" 1816 | }, 1817 | "exclude-from-classmap": [ 1818 | "/Tests/" 1819 | ] 1820 | }, 1821 | "notification-url": "https://packagist.org/downloads/", 1822 | "license": [ 1823 | "MIT" 1824 | ], 1825 | "authors": [ 1826 | { 1827 | "name": "Fabien Potencier", 1828 | "email": "fabien@symfony.com" 1829 | }, 1830 | { 1831 | "name": "Symfony Community", 1832 | "homepage": "https://symfony.com/contributors" 1833 | } 1834 | ], 1835 | "description": "Symfony Debug Component", 1836 | "homepage": "https://symfony.com", 1837 | "time": "2017-03-28T21:38:24+00:00" 1838 | }, 1839 | { 1840 | "name": "symfony/filesystem", 1841 | "version": "v3.2.7", 1842 | "source": { 1843 | "type": "git", 1844 | "url": "https://github.com/symfony/filesystem.git", 1845 | "reference": "64421e6479c4a8e60d790fb666bd520992861b66" 1846 | }, 1847 | "dist": { 1848 | "type": "zip", 1849 | "url": "https://api.github.com/repos/symfony/filesystem/zipball/64421e6479c4a8e60d790fb666bd520992861b66", 1850 | "reference": "64421e6479c4a8e60d790fb666bd520992861b66", 1851 | "shasum": "" 1852 | }, 1853 | "require": { 1854 | "php": ">=5.5.9" 1855 | }, 1856 | "type": "library", 1857 | "extra": { 1858 | "branch-alias": { 1859 | "dev-master": "3.2-dev" 1860 | } 1861 | }, 1862 | "autoload": { 1863 | "psr-4": { 1864 | "Symfony\\Component\\Filesystem\\": "" 1865 | }, 1866 | "exclude-from-classmap": [ 1867 | "/Tests/" 1868 | ] 1869 | }, 1870 | "notification-url": "https://packagist.org/downloads/", 1871 | "license": [ 1872 | "MIT" 1873 | ], 1874 | "authors": [ 1875 | { 1876 | "name": "Fabien Potencier", 1877 | "email": "fabien@symfony.com" 1878 | }, 1879 | { 1880 | "name": "Symfony Community", 1881 | "homepage": "https://symfony.com/contributors" 1882 | } 1883 | ], 1884 | "description": "Symfony Filesystem Component", 1885 | "homepage": "https://symfony.com", 1886 | "time": "2017-03-26T15:47:15+00:00" 1887 | }, 1888 | { 1889 | "name": "symfony/polyfill-mbstring", 1890 | "version": "v1.3.0", 1891 | "source": { 1892 | "type": "git", 1893 | "url": "https://github.com/symfony/polyfill-mbstring.git", 1894 | "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4" 1895 | }, 1896 | "dist": { 1897 | "type": "zip", 1898 | "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/e79d363049d1c2128f133a2667e4f4190904f7f4", 1899 | "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4", 1900 | "shasum": "" 1901 | }, 1902 | "require": { 1903 | "php": ">=5.3.3" 1904 | }, 1905 | "suggest": { 1906 | "ext-mbstring": "For best performance" 1907 | }, 1908 | "type": "library", 1909 | "extra": { 1910 | "branch-alias": { 1911 | "dev-master": "1.3-dev" 1912 | } 1913 | }, 1914 | "autoload": { 1915 | "psr-4": { 1916 | "Symfony\\Polyfill\\Mbstring\\": "" 1917 | }, 1918 | "files": [ 1919 | "bootstrap.php" 1920 | ] 1921 | }, 1922 | "notification-url": "https://packagist.org/downloads/", 1923 | "license": [ 1924 | "MIT" 1925 | ], 1926 | "authors": [ 1927 | { 1928 | "name": "Nicolas Grekas", 1929 | "email": "p@tchwork.com" 1930 | }, 1931 | { 1932 | "name": "Symfony Community", 1933 | "homepage": "https://symfony.com/contributors" 1934 | } 1935 | ], 1936 | "description": "Symfony polyfill for the Mbstring extension", 1937 | "homepage": "https://symfony.com", 1938 | "keywords": [ 1939 | "compatibility", 1940 | "mbstring", 1941 | "polyfill", 1942 | "portable", 1943 | "shim" 1944 | ], 1945 | "time": "2016-11-14T01:06:16+00:00" 1946 | }, 1947 | { 1948 | "name": "symfony/stopwatch", 1949 | "version": "v3.2.7", 1950 | "source": { 1951 | "type": "git", 1952 | "url": "https://github.com/symfony/stopwatch.git", 1953 | "reference": "c5ee0f8650c84b4d36a5f76b3b504233feaabf75" 1954 | }, 1955 | "dist": { 1956 | "type": "zip", 1957 | "url": "https://api.github.com/repos/symfony/stopwatch/zipball/c5ee0f8650c84b4d36a5f76b3b504233feaabf75", 1958 | "reference": "c5ee0f8650c84b4d36a5f76b3b504233feaabf75", 1959 | "shasum": "" 1960 | }, 1961 | "require": { 1962 | "php": ">=5.5.9" 1963 | }, 1964 | "type": "library", 1965 | "extra": { 1966 | "branch-alias": { 1967 | "dev-master": "3.2-dev" 1968 | } 1969 | }, 1970 | "autoload": { 1971 | "psr-4": { 1972 | "Symfony\\Component\\Stopwatch\\": "" 1973 | }, 1974 | "exclude-from-classmap": [ 1975 | "/Tests/" 1976 | ] 1977 | }, 1978 | "notification-url": "https://packagist.org/downloads/", 1979 | "license": [ 1980 | "MIT" 1981 | ], 1982 | "authors": [ 1983 | { 1984 | "name": "Fabien Potencier", 1985 | "email": "fabien@symfony.com" 1986 | }, 1987 | { 1988 | "name": "Symfony Community", 1989 | "homepage": "https://symfony.com/contributors" 1990 | } 1991 | ], 1992 | "description": "Symfony Stopwatch Component", 1993 | "homepage": "https://symfony.com", 1994 | "time": "2017-02-18T17:28:00+00:00" 1995 | }, 1996 | { 1997 | "name": "symfony/yaml", 1998 | "version": "v3.2.7", 1999 | "source": { 2000 | "type": "git", 2001 | "url": "https://github.com/symfony/yaml.git", 2002 | "reference": "62b4cdb99d52cb1ff253c465eb1532a80cebb621" 2003 | }, 2004 | "dist": { 2005 | "type": "zip", 2006 | "url": "https://api.github.com/repos/symfony/yaml/zipball/62b4cdb99d52cb1ff253c465eb1532a80cebb621", 2007 | "reference": "62b4cdb99d52cb1ff253c465eb1532a80cebb621", 2008 | "shasum": "" 2009 | }, 2010 | "require": { 2011 | "php": ">=5.5.9" 2012 | }, 2013 | "require-dev": { 2014 | "symfony/console": "~2.8|~3.0" 2015 | }, 2016 | "suggest": { 2017 | "symfony/console": "For validating YAML files using the lint command" 2018 | }, 2019 | "type": "library", 2020 | "extra": { 2021 | "branch-alias": { 2022 | "dev-master": "3.2-dev" 2023 | } 2024 | }, 2025 | "autoload": { 2026 | "psr-4": { 2027 | "Symfony\\Component\\Yaml\\": "" 2028 | }, 2029 | "exclude-from-classmap": [ 2030 | "/Tests/" 2031 | ] 2032 | }, 2033 | "notification-url": "https://packagist.org/downloads/", 2034 | "license": [ 2035 | "MIT" 2036 | ], 2037 | "authors": [ 2038 | { 2039 | "name": "Fabien Potencier", 2040 | "email": "fabien@symfony.com" 2041 | }, 2042 | { 2043 | "name": "Symfony Community", 2044 | "homepage": "https://symfony.com/contributors" 2045 | } 2046 | ], 2047 | "description": "Symfony Yaml Component", 2048 | "homepage": "https://symfony.com", 2049 | "time": "2017-03-20T09:45:15+00:00" 2050 | }, 2051 | { 2052 | "name": "webmozart/assert", 2053 | "version": "1.2.0", 2054 | "source": { 2055 | "type": "git", 2056 | "url": "https://github.com/webmozart/assert.git", 2057 | "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f" 2058 | }, 2059 | "dist": { 2060 | "type": "zip", 2061 | "url": "https://api.github.com/repos/webmozart/assert/zipball/2db61e59ff05fe5126d152bd0655c9ea113e550f", 2062 | "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f", 2063 | "shasum": "" 2064 | }, 2065 | "require": { 2066 | "php": "^5.3.3 || ^7.0" 2067 | }, 2068 | "require-dev": { 2069 | "phpunit/phpunit": "^4.6", 2070 | "sebastian/version": "^1.0.1" 2071 | }, 2072 | "type": "library", 2073 | "extra": { 2074 | "branch-alias": { 2075 | "dev-master": "1.3-dev" 2076 | } 2077 | }, 2078 | "autoload": { 2079 | "psr-4": { 2080 | "Webmozart\\Assert\\": "src/" 2081 | } 2082 | }, 2083 | "notification-url": "https://packagist.org/downloads/", 2084 | "license": [ 2085 | "MIT" 2086 | ], 2087 | "authors": [ 2088 | { 2089 | "name": "Bernhard Schussek", 2090 | "email": "bschussek@gmail.com" 2091 | } 2092 | ], 2093 | "description": "Assertions to validate method input/output with nice error messages.", 2094 | "keywords": [ 2095 | "assert", 2096 | "check", 2097 | "validate" 2098 | ], 2099 | "time": "2016-11-23T20:04:58+00:00" 2100 | }, 2101 | { 2102 | "name": "wpackagist-plugin/timber-library", 2103 | "version": "1.2.4", 2104 | "source": { 2105 | "type": "svn", 2106 | "url": "https://plugins.svn.wordpress.org/timber-library/", 2107 | "reference": "tags/1.2.4" 2108 | }, 2109 | "dist": { 2110 | "type": "zip", 2111 | "url": "https://downloads.wordpress.org/plugin/timber-library.1.2.4.zip", 2112 | "reference": null, 2113 | "shasum": null 2114 | }, 2115 | "require": { 2116 | "composer/installers": "~1.0" 2117 | }, 2118 | "type": "wordpress-plugin", 2119 | "homepage": "https://wordpress.org/plugins/timber-library/" 2120 | } 2121 | ], 2122 | "aliases": [], 2123 | "minimum-stability": "stable", 2124 | "stability-flags": { 2125 | "satooshi/php-coveralls": 20 2126 | }, 2127 | "prefer-stable": false, 2128 | "prefer-lowest": false, 2129 | "platform": { 2130 | "php": ">=5.3.0" 2131 | }, 2132 | "platform-dev": [] 2133 | } 2134 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const $ = require('gulp-load-plugins')(); 3 | const config = require('./package.json'); 4 | 5 | ////////////////////////////////////////////// 6 | // 7 | // Front-end build tasks 8 | // 9 | //////////////////////////////////////////////// 10 | 11 | // ---------------------------------------- 12 | // 13 | // Styles: Compile scss to css and minify. 14 | // 15 | // ---------------------------------------- 16 | 17 | gulp.task('styles', function() { 18 | gulp.src('./assets/src/scss/*.scss') 19 | .pipe($.sass()) 20 | .pipe($.cleanCss()) 21 | .on('error', $.sass.logError) 22 | .pipe( gulp.dest('./assets/build/css/') ); 23 | }); 24 | 25 | // ---------------------------------------- 26 | // 27 | // Scripts: Minify js. 28 | // 29 | // ---------------------------------------- 30 | gulp.task('scripts', function() { 31 | return gulp.src('./assets/src/js/*.js') 32 | .pipe($.uglify()) 33 | .pipe(gulp.dest('./assets/build/js/')); 34 | }); 35 | 36 | // ---------------------------------------- 37 | // 38 | // Watch/Default: Watch asset folders for 39 | // changes and rerun tasks as needed. 40 | // 41 | // ---------------------------------------- 42 | gulp.task('watch', function() { 43 | gulp.watch('./assets/src/scss/**/*.scss', ['styles']); 44 | gulp.watch('./assets/src/js/**/*.js', ['scripts']); 45 | }); 46 | 47 | gulp.task('default', ['styles', 'scripts', 'watch']); 48 | 49 | gulp.task('build', ['styles', 'scripts']); 50 | 51 | ////////////////////////////////////////////// 52 | // 53 | // Automated deployment to wp.org plugin repo 54 | // 55 | ////////////////////////////////////////////// 56 | 57 | const WP_REPO = 'http://plugins.svn.wordpress.org/stream-manager/'; 58 | const ENTRY_FILE = 'stream-manager.php'; 59 | const BUILD_FILES = [ 60 | './assets/**/*', 61 | './includes/**/*', 62 | './README.txt', 63 | './README.md', 64 | './stream-manager.php' 65 | ]; 66 | 67 | // ---------------------------------------- 68 | // 69 | // Git: Checkout the master branch from git 70 | // and pull down the latest. 71 | // 72 | // ---------------------------------------- 73 | 74 | gulp.task('git:checkout', function(done) { 75 | return $.git.checkout('master', function (err) { 76 | if (err) throw err; 77 | done(); 78 | }); 79 | }); 80 | 81 | gulp.task('git:pull', function(done) { 82 | return $.git.pull('origin', 'master', function (err) { 83 | if (err) throw err; 84 | done(); 85 | }); 86 | }); 87 | 88 | gulp.task('git', $.sequence('git:checkout', 'git:pull')); 89 | 90 | // ---------------------------------------- 91 | // 92 | // Version: Update the version number and 93 | // commit and push the update 94 | // 95 | // ---------------------------------------- 96 | 97 | gulp.task('version:plugin', function() { 98 | return gulp.src([ENTRY_FILE], { base: './' }) 99 | .pipe($.replace(/(Version:\s+)([\d|.]+)/, '$1' + config.version)) 100 | .pipe(gulp.dest('.')); 101 | }); 102 | 103 | gulp.task('version:readme', function() { 104 | return gulp.src(['README.txt'], { base: './' }) 105 | .pipe($.replace(/(Stable tag:\s+)([\d|.]+)/, '$1' + config.version)) 106 | .pipe(gulp.dest('.')); 107 | }); 108 | 109 | gulp.task('version:commit', function() { 110 | return gulp.src([ENTRY_FILE, 'README.txt']) 111 | .pipe($.git.commit('Updated version #')); 112 | }); 113 | 114 | gulp.task('version:push', function() { 115 | return $.git.push('origin', 'master', function (err) { 116 | if (err) throw err; 117 | }); 118 | }) 119 | 120 | gulp.task('version', $.sequence( 121 | 'version:plugin', 122 | 'version:readme', 123 | 'version:commit', 124 | 'version:push' 125 | )); 126 | 127 | // ---------------------------------------- 128 | // 129 | // Svn: Checkout the SVN repository from wp.org. 130 | // Clean out what's already in the tag 131 | // and trunk folders, copy our files into 132 | // the svn repo, and commit the updates. 133 | // 134 | // ---------------------------------------- 135 | 136 | gulp.task('svn:checkout', function(done) { 137 | return $.svn.checkout(WP_REPO, 'svn', function(err){ 138 | if(err) throw err; 139 | done(); 140 | }); 141 | }); 142 | 143 | gulp.task('svn:delete', function() { 144 | return gulp.src(['./svn/tags/' + config.version + '/*', './svn/trunk/*'], {read:false}) 145 | .pipe($.clean()); 146 | }); 147 | 148 | gulp.task('svn:copy', ['build'], function() { 149 | return gulp.src(BUILD_FILES, { base: './' }) 150 | .pipe(gulp.dest('./svn/tags/' + config.version)) 151 | .pipe(gulp.dest('./svn/trunk')); 152 | }); 153 | 154 | gulp.task('svn:add', function(done){ 155 | return $.svn.add('svn/*', {args: '--force'}, function(err){ 156 | if(err) throw err; 157 | done(); 158 | }); 159 | }); 160 | 161 | gulp.task('svn:commit', function(done){ 162 | return $.svn.commit('Releasing tag ' + config.version, {cwd: './svn'}, function(err){ 163 | if(err) throw err; 164 | done(); 165 | }); 166 | }); 167 | 168 | gulp.task('svn:cleanup', function() { 169 | return gulp.src('./svn', {read:false}) 170 | .pipe($.clean()); 171 | }); 172 | 173 | gulp.task('svn', $.sequence( 174 | 'svn:checkout', 175 | 'svn:delete', 176 | 'svn:copy', 177 | 'svn:add', 178 | 'svn:commit', 179 | 'svn:cleanup' 180 | )); 181 | 182 | // ------------------------------------- 183 | // 184 | // Everything! Release the plugin to the wp plugin repo. 185 | // 186 | // ------------------------------------- 187 | 188 | gulp.task('release', $.sequence('git', 'version', 'svn')); 189 | -------------------------------------------------------------------------------- /includes/class-stream-manager-admin.php: -------------------------------------------------------------------------------- 1 | 'post', 39 | 'post_status' => 'publish', 40 | 'has_password' => false, 41 | 'ignore_sticky_posts' => true, 42 | 43 | 'posts_per_page' => 100, 44 | 'orderby' => 'post__in' 45 | ); 46 | 47 | /** 48 | * Initialize the plugin 49 | * 50 | * @since 1.0.0 51 | */ 52 | private function __construct() { 53 | $this->plugin = StreamManager::get_instance(); 54 | $this->plugin_slug = $this->plugin->get_plugin_slug(); 55 | $this->post_type_slug = $this->plugin->get_post_type_slug(); 56 | 57 | 58 | // Admin Page Helpers 59 | // ------------------ 60 | 61 | // Load admin styles and scripts 62 | add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_styles' ) ); 63 | add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) ); 64 | 65 | // Stream edit page metaboxes 66 | add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ) ); 67 | 68 | // Help text 69 | add_action( 'admin_head', array( $this, 'add_help_text' ), 10, 3 ); 70 | 71 | 72 | // Stream Manipulation 73 | // ----------------- 74 | 75 | // Saving Streams 76 | add_action( 'save_post', array( $this, 'save_stream' ) ); 77 | 78 | // AJAX Helpers 79 | // ------------ 80 | 81 | // Heartbeat 82 | add_filter( 'heartbeat_received', array( $this, 'ajax_heartbeat' ), 10, 3 ); 83 | 84 | // Retrieve rendered post stubs AJAX 85 | add_filter( 'wp_ajax_sm_request', array( $this, 'ajax_retrieve_posts' ) ); 86 | 87 | // Search posts AJAX 88 | add_filter( 'wp_ajax_sm_search', array( $this, 'ajax_search_posts' ) ); 89 | 90 | // Retrieve posts for stream reload 91 | add_filter( 'wp_ajax_sm_reload', array( $this, 'ajax_retrieve_reload_posts' ) ); 92 | 93 | add_filter( 'wp_terms_checklist_args', array( $this, 'stream_categories_helper' ), 10, 2 ); 94 | } 95 | 96 | public static function is_active() { 97 | if ( function_exists( 'get_current_screen' ) ) { 98 | $current_screen = get_current_screen(); 99 | if ( isset($current_screen) ) { 100 | return $current_screen->id == "sm_stream"; 101 | } 102 | } 103 | } 104 | 105 | /** 106 | * Return an instance of this class. 107 | * 108 | * @since 1.0.0 109 | * 110 | * @return object A single instance of this class. 111 | */ 112 | public static function get_instance() { 113 | 114 | // If the single instance hasn't been set, set it now. 115 | if ( null == self::$instance ) { 116 | self::$instance = new self; 117 | } 118 | 119 | return self::$instance; 120 | } 121 | 122 | /** 123 | * Register and enqueue admin-specific style sheet. 124 | * 125 | * @since 1.0.0 126 | * 127 | * @return null Return early if no settings page is registered. 128 | */ 129 | public function enqueue_admin_styles() { 130 | if ( !$this->is_active() ) return; 131 | 132 | wp_enqueue_style( 133 | $this->plugin_slug .'-admin-styles', 134 | plugins_url( '../assets/build/css/style.css', __FILE__ ), 135 | array(), 136 | StreamManager::VERSION 137 | ); 138 | 139 | /* 140 | * this limits the number of visible stream items via css 141 | * when one is deleted, the stubs "move up", so they one at a time become visible 142 | * https://github.com/Upstatement/stream-manager/issues/28 143 | */ 144 | //+ 1 because css is greedy! (it encompasses the number, so we want the *next* element) 145 | $display_limit = (int) apply_filters( $this->plugin_slug . '/stub_display_limit', 15 ) + 1; 146 | $display_limit_css = ".sm-posts .content:nth-of-type(n+{$display_limit}){ 147 | opacity:0.4; 148 | }"; 149 | wp_add_inline_style( $this->plugin_slug .'-admin-styles', $display_limit_css ); 150 | } 151 | 152 | /** 153 | * Register and enqueue admin-specific JavaScript. 154 | * 155 | * @since 1.0.0 156 | * 157 | * @return null Return early if no settings page is registered. 158 | */ 159 | public function enqueue_admin_scripts() { 160 | if ( !$this->is_active() ) return; 161 | 162 | wp_enqueue_script( 163 | $this->plugin_slug . '-admin-script', 164 | plugins_url( '../assets/build/js/script.js', __FILE__ ), 165 | array( 'jquery', 'underscore' ), 166 | StreamManager::VERSION 167 | ); 168 | } 169 | 170 | 171 | /** 172 | * Add meta boxes to Stream edit page 173 | * 174 | * @since 1.0.0 175 | */ 176 | public function add_meta_boxes() { 177 | add_meta_box( 178 | 'stream_box_stream', 179 | 'Stream', 180 | array( $this, 'meta_box_stream' ), 181 | $this->post_type_slug, 182 | 'normal' 183 | ); 184 | add_meta_box( 185 | 'stream_box_add', 186 | 'Add Post', 187 | array( $this, 'meta_box_add' ), 188 | $this->post_type_slug, 189 | 'side' 190 | ); 191 | add_meta_box( 192 | 'stream_box_zones', 193 | 'Zones', 194 | array( $this, 'meta_box_zones' ), 195 | $this->post_type_slug, 196 | 'side' 197 | ); 198 | } 199 | 200 | 201 | /** 202 | * Render Stream metabox 203 | * 204 | * @since 1.0.0 205 | * 206 | * @param object $post WordPress post object 207 | */ 208 | public function meta_box_stream( $post ) { 209 | $stream_post = new TimberStream( $post->ID ); 210 | $ids = array_keys( $stream_post->filter_stream('pinned', false) ); 211 | $pinned = array_keys( $stream_post->filter_stream('pinned', true ) ); 212 | $layouts = $stream_post->get('layouts'); 213 | $layout = $layouts['layouts'][ $layouts['active'] ]; 214 | 215 | Timber::render('views/stream.twig', array( 216 | 'posts' => $stream_post->get_posts( array( 'show_hidden' => true ) ), 217 | 'post_ids' => implode( ',', $ids ), 218 | 'post_pinned' => implode( ',', $pinned ), 219 | 'nonce' => wp_nonce_field('sm_nonce', 'sm_meta_box_nonce', true, false), 220 | 'layout' => $layout 221 | )); 222 | } 223 | 224 | 225 | /** 226 | * Render Post Add metabox 227 | * 228 | * @since 1.0.0 229 | * 230 | * @param object $post WordPress post object 231 | */ 232 | public function meta_box_add( $post ) { 233 | Timber::render('views/add.twig'); 234 | } 235 | 236 | 237 | /** 238 | * Render Layout metabox 239 | * 240 | * @since 1.0.0 241 | * 242 | * @param object $post WordPress post object 243 | */ 244 | public function meta_box_zones( $post ) { 245 | $stream_post = new TimberStream( $post->ID ); 246 | $layouts = $stream_post->get('layouts'); 247 | 248 | $context = array( 249 | 'post' => $stream_post, 250 | 'layouts' => $layouts, 251 | 'layouts_json' => JSON_encode( $layouts ) 252 | ); 253 | 254 | Timber::render('views/zones.twig', array_merge(Timber::get_context(), $context)); 255 | } 256 | 257 | /** 258 | * Save the stream metadata 259 | * 260 | * @since 1.0.0 261 | * @param integer $stream_id Stream Post ID 262 | * @todo Move Rules update to TimberStream::save_stream 263 | */ 264 | public function save_stream( $stream_id, $apply_security_checks = true ) { 265 | // Bail if we're doing an auto save 266 | if( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return; 267 | 268 | if ( $apply_security_checks ) { 269 | 270 | // if our nonce isn't there, or we can't verify it, bail 271 | if( !isset( $_POST['sm_meta_box_nonce'] ) || !wp_verify_nonce( $_POST['sm_meta_box_nonce'], 'sm_nonce' ) ) return; 272 | 273 | // if our current user can't edit this post, bail 274 | if( !current_user_can( 'edit_post', $stream_id ) ) return; 275 | } 276 | 277 | $stream = new TimberStream( $stream_id ); 278 | 279 | // Sorting 280 | if ( isset( $_POST['sm_sort'] ) ) { 281 | $data = array(); 282 | 283 | foreach ( $_POST['sm_sort'] as $i => $post_id ) { 284 | $data[] = array( 285 | 'id' => $post_id, 286 | 'pinned' => isset($_POST['sm_pin'][$post_id]) 287 | ); 288 | } 289 | 290 | $stream->set('stream', $data); 291 | $stream->repopulate_stream(); 292 | } 293 | 294 | // Layouts 295 | if ( isset( $_POST['sm_layouts'] ) ) { 296 | $stream->set('layouts', JSON_decode( stripslashes($_POST['sm_layouts']), true ) ); 297 | } 298 | 299 | // Save the stream, and prevent and infinite loop 300 | remove_action( 'save_post', array( $this, 'save_stream' ) ); 301 | $stream->save_stream(); 302 | add_action( 'save_post', array( $this, 'save_stream' ) ); 303 | } 304 | 305 | 306 | /** 307 | * Add help text to stream edit page 308 | * 309 | * @since 1.0.0 310 | */ 311 | function add_help_text() { 312 | $screen = get_current_screen(); 313 | 314 | // Return early if we're not on the book post type. 315 | if ( 'sm_stream' != $screen->id ) return; 316 | 317 | // Setup help tab args. 318 | $tabs = array( 319 | array( 320 | 'id' => 'sm_stream_1', 321 | 'title' => 'About Streams', 322 | 'content' => implode("\n", array( 323 | '

    About Streams

    ', 324 | '

    Streams are feeds of content, updated automatically as new posts are created.', 325 | 'Any changes made to the stream from this page will be reflected where the stream is output in the theme, on the homepage or elsewhere.

    ' 326 | )) 327 | ), 328 | array( 329 | 'id' => 'sm_stream_2', 330 | 'title' => 'How to Use', 331 | 'content' => implode("\n", array( 332 | '

    How to Use

    ', 333 | '

    Reordering: ', 334 | 'Drag and drop posts into the desired order, then click the "Update" button.

    ', 335 | '

    Adding a Post: ', 336 | 'Type the name of the post in the "Add Post" box and select the intended post when it appears in the dropdown.

    ', 337 | '

    Removing a Post: ', 338 | 'To remove a post from the stream, hover over the post and click the x in the upper right. Note that the post will not be deleted entirely; instead, it will be removed from its current position and appended to the bottom of the stream.', 339 | '

    For more detailed instructions, consult the User Guide in the Github project readme.

    ' 340 | )) 341 | ), 342 | array( 343 | 'id' => 'sm_stream_3', 344 | 'title' => 'Use in Theme', 345 | 'content' => implode("\n", array( 346 | '

    Use in Theme

    ', 347 | '

    ', 348 | '

    $context[\'stream\'] = new TimberStream(' . get_the_ID() . ');
    ', 349 | '

    ', 350 | '

    In your view file (twig):

    ', 351 | '

    {% for post in stream.get_posts %}',
    352 | 		    	'  {{ post.title }}',
    353 | 		    	'{% endfor %}

    ' 354 | )) 355 | ) 356 | ); 357 | 358 | // Add the help tab. 359 | foreach ( $tabs as $tab ) { 360 | $screen->add_help_tab( $tab ); 361 | } 362 | } 363 | 364 | public function stream_categories_helper( $args, $post_id ) { 365 | if ( $this->is_active() ) { 366 | $stream = new TimberStream( $post_id ); 367 | if ( isset($stream->sm_rules['category']) ) { 368 | $args['selected_cats'] = $stream->sm_rules['category']; 369 | } 370 | } 371 | return $args; 372 | } 373 | 374 | 375 | /** 376 | * Respond to admin heartbeat with stream IDs 377 | * 378 | * @since 1.0.0 379 | * 380 | * @param array $response default WordPress heartbeat response 381 | * @param array $data data included with WordPress heartbeat request 382 | * @param string $screen_id admin screen slug 383 | * 384 | * @return array WordPress heartbeat response 385 | */ 386 | public function ajax_heartbeat( $response, $data, $screen_id ) { 387 | 388 | if ( $screen_id == 'sm_stream' && isset( $data['wp-refresh-post-lock'] ) ) { 389 | $stream_post = new TimberStream( $data['wp-refresh-post-lock']['post_id'] ); 390 | $ids = array_keys( $stream_post->filter_stream('pinned', false) ); 391 | $response['sm_ids'] = implode( ',', $ids ); 392 | 393 | $pinned = array_keys( $stream_post->filter_stream('pinned', true) ); 394 | $response['sm_pinned'] = implode( ',', $pinned ); 395 | } 396 | 397 | return $response; 398 | } 399 | 400 | 401 | /** 402 | * Retrieve rendered post stubs 403 | * 404 | * @since 1.0.0 405 | * 406 | * @param array $request AJAX request (uses $_POST instead) 407 | */ 408 | public function ajax_retrieve_posts( $request ) { 409 | if ( !isset( $_POST['queue'] ) ) $this->ajax_respond( 'error' ); 410 | $output = StreamManagerAjaxHelper::retrieve_posts($_POST['queue']); 411 | 412 | $this->ajax_respond( 'success', $output ); 413 | } 414 | 415 | /** 416 | * Retrieve search results 417 | * 418 | * @since 1.0.0 419 | * 420 | * @param array $request AJAX request (uses $_POST instead) 421 | */ 422 | public function ajax_search_posts( $request ) { 423 | if ( !isset( $_POST['query'] ) || !isset( $_POST['stream_id'] ) ) $this->ajax_respond( 'error' ); 424 | $output = StreamManagerAjaxHelper::search_posts($_POST['query'], $_POST['stream_id']); 425 | 426 | $this->ajax_respond( 'success', $output ); 427 | } 428 | 429 | /** 430 | * Send AJAX response 431 | * 432 | * @since 1.0.0 433 | * 434 | * @param string $status AJAX status (error|success) 435 | * @param array $data data with which to respond 436 | */ 437 | public function ajax_respond( $status = 'error', $data = array() ) { 438 | echo( json_encode( array( 439 | 'status' => $status, 440 | 'data' => $data 441 | ))); 442 | die(); 443 | } 444 | 445 | 446 | } 447 | -------------------------------------------------------------------------------- /includes/class-stream-manager-ajax-helper.php: -------------------------------------------------------------------------------- 1 | $item) { 32 | $post = new TimberPost( $item['id'] ); 33 | if ( !$post ) continue; 34 | $post->pinned = false; 35 | $output[ $item['id'] ] = array( 36 | 'position' => $item['position'], 37 | 'object' => Timber::compile('views/stub.twig', array( 38 | 'post' => $post 39 | )) 40 | ); 41 | } 42 | return $output; 43 | } 44 | 45 | /** 46 | * Searches posts for 'Add New' autocomplete 47 | * 48 | * @since 1.0.0 49 | * 50 | * @param string $query search term 51 | * @param int $stream_id post id of current stream 52 | * 53 | * @return array $output posts w/ ids, date, title, human time diff 54 | */ 55 | public static function search_posts( $query, $stream_id ) { 56 | $defaults = array( 57 | 's' => $query, 58 | 'post_type' => 'post', 59 | 'post_status' => 'publish', 60 | 'posts_per_page' => 10 61 | ); 62 | $stream = new TimberStream($stream_id); 63 | $args = array_merge( $defaults, $stream->get( 'query' ) ); 64 | 65 | $posts = Timber::get_posts( $args ); 66 | 67 | $output = array(); 68 | 69 | foreach ( $posts as $post ) { 70 | $output[] = array( 71 | 'id' => $post->ID, 72 | 'title' => $post->title, 73 | 'date' => $post->post_date, 74 | 'human_date' => human_time_diff( strtotime( $post->post_date ) ) 75 | ); 76 | } 77 | return $output; 78 | } 79 | 80 | } -------------------------------------------------------------------------------- /includes/class-stream-manager-api.php: -------------------------------------------------------------------------------- 1 | 'sm_stream', 'name' => $slug ) ); 27 | if ( count( $posts ) ) { 28 | return true; 29 | } 30 | return false; 31 | } 32 | 33 | /** 34 | * Insert a new stream, with the option to pass a wp_query array to filter the stream. 35 | * Returns false if the stream already exists. 36 | * 37 | * @param string $slug 38 | * @param string $title 39 | * @param array $query_array wp_query object 40 | * 41 | * @return int $pid ID of new stream 42 | */ 43 | static function insert_stream( $slug, $title = NULL, $query_array = NULL ) { 44 | 45 | if( self::stream_exists( $slug ) ) { 46 | return false; 47 | } 48 | 49 | $post_title = $title?: $slug; 50 | $args = array( 51 | 'post_type' => 'sm_stream', 52 | 'post_name' => $slug, 53 | 'post_title' => $post_title, 54 | 'post_status' => 'publish' 55 | ); 56 | $pid = wp_insert_post($args); 57 | 58 | if ( $query_array ) { 59 | add_filter('stream-manager/options/'.$slug, function($defaults) use ($query_array) { 60 | $defaults['query'] = array_merge( $defaults['query'], $query_array ); 61 | return $defaults; 62 | }); 63 | } 64 | 65 | return $pid; 66 | } 67 | 68 | /** 69 | * Delete a stream by slug 70 | * 71 | * @param string $slug 72 | * @param bool $force_delete bypass trash and force deletion 73 | * 74 | * @return int $deleted ID of deleted stream 75 | */ 76 | static function delete_stream( $slug, $force_delete = true ) { 77 | $posts = get_posts( array( 'post_type' => 'sm_stream', 'name' => $slug ) ); 78 | if( $posts ) { 79 | $post = $posts[0]; 80 | $deleted = wp_delete_post( $post->ID, $force_delete ); 81 | return $deleted->ID; 82 | } else { 83 | return false; 84 | } 85 | } 86 | } 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /includes/class-stream-manager-manager.php: -------------------------------------------------------------------------------- 1 | plugin = StreamManager::get_instance(); 40 | $this->plugin_slug = $this->plugin->get_plugin_slug(); 41 | $this->post_type_slug = $this->plugin->get_post_type_slug(); 42 | 43 | // Saving Posts (= updating streams) 44 | add_action( 'transition_post_status', array( $this, 'on_save_post' ), 10, 3 ); 45 | add_action( 'publish_future_post', function($post_id) { 46 | $post = get_post($post_id); 47 | $this->on_save_post('publish', 'future', $post); 48 | }, 10, 1); 49 | 50 | } 51 | 52 | /** 53 | * Return an instance of this class. 54 | * 55 | * @since 1.1.0 56 | * 57 | * @return object A single instance of this class. 58 | */ 59 | public static function get_instance() { 60 | 61 | // If the single instance hasn't been set, set it now. 62 | if ( null == self::$instance ) { 63 | self::$instance = new self; 64 | } 65 | 66 | return self::$instance; 67 | } 68 | 69 | 70 | /** 71 | * Update streams whenever any post status is changed 72 | * 73 | * @since 1.1.0 74 | * 75 | * @param string $new new post status 76 | * @param string $old old post status 77 | * @param object $post WordPress post object 78 | */ 79 | public function on_save_post( $new, $old, $post ) { 80 | if ( $post->post_type == 'sm_stream' ) return; 81 | 82 | if ( $old == 'publish' && $new != 'publish' ) { 83 | // Remove from streams 84 | $streams = $this->plugin->get_streams(); 85 | foreach ( $streams as $stream ) { 86 | $stream->remove_post( $post->ID ); 87 | } 88 | } 89 | 90 | if ( $old != 'publish' && $new == 'publish' ) { 91 | //seems weird, but it's necessary for ACF 92 | //and potentially other plugins 93 | //we can't be sure what actions have been added 94 | //so checking for infinite loop-type bugs isn't possible 95 | do_action('save_post', $post->ID, $post, true ); 96 | remove_all_actions('save_post'); 97 | // Add to streams 98 | $streams = $this->plugin->get_streams(); 99 | foreach ( $streams as $stream ) { 100 | $stream->insert_post( $post->ID ); 101 | } 102 | } 103 | } 104 | 105 | 106 | } 107 | -------------------------------------------------------------------------------- /includes/class-stream-manager-utilities.php: -------------------------------------------------------------------------------- 1 | term_id; 42 | } 43 | 44 | return $return_objects ? $terms : $output; 45 | } 46 | 47 | public static function build_tax_query( $taxonomies ) { 48 | $output = array('relation' => 'OR'); 49 | foreach ( $taxonomies as $taxonomy => $terms ) { 50 | if ( !$terms ) continue; 51 | 52 | $terms = is_array($terms) ? $terms : self::parse_terms( $taxonomy, $terms ); 53 | foreach ( $terms as $i => $term ) { 54 | if ( empty( $term ) ) unset( $terms[$i] ); 55 | } 56 | 57 | if ( !empty($terms) ) { 58 | $output[] = array( 59 | 'taxonomy' => $taxonomy, 60 | 'field' => 'term_id', 61 | 'terms' => $terms 62 | ); 63 | } 64 | } 65 | return $output; 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /includes/class-stream-manager.php: -------------------------------------------------------------------------------- 1 | plugin_slug; 90 | } 91 | 92 | /** 93 | * Return the post type slug. 94 | * 95 | * @since 1.0.0 96 | * 97 | * @return Post type slug variable. 98 | */ 99 | public function get_post_type_slug() { 100 | return $this->post_type_slug; 101 | } 102 | 103 | /** 104 | * Return an instance of this class. 105 | * 106 | * @since 1.0.0 107 | * 108 | * @return object A single instance of this class. 109 | */ 110 | public static function get_instance() { 111 | 112 | // If the single instance hasn't been set, set it now. 113 | if ( null == self::$instance ) { 114 | self::$instance = new self; 115 | } 116 | 117 | return self::$instance; 118 | } 119 | 120 | /** 121 | * Ensure that Timber is loaded. Depending on the order that the 122 | * plugins are activated, Timber may be loaded after the Stream 123 | * Manager and needs to be loaded manually. 124 | * 125 | * @since 1.0.0 126 | * 127 | * @return boolean True if dependencies are met, false if not 128 | */ 129 | public function check_dependencies() { 130 | return class_exists('Timber'); 131 | } 132 | 133 | /** 134 | * Create the Stream post type, add to admin 135 | * 136 | * @since 1.0.0 137 | */ 138 | public function define_post_types() { 139 | $labels = array( 140 | 'name' => 'Streams', 141 | 'singular_name' => 'Stream', 142 | 'menu_name' => 'Streams', 143 | 'parent_item_colon' => 'Parent Stream', 144 | 'all_items' => 'Streams', 145 | 'view_item' => 'View Stream', 146 | 'add_new_item' => 'Add New Stream', 147 | 'add_new' => 'Add New', 148 | 'edit_item' => 'Edit Stream', 149 | 'update_item' => 'Update Stream', 150 | 'search_items' => 'Search Stream', 151 | 'not_found' => 'Not found', 152 | 'not_found_in_trash' => 'Not found in Trash', 153 | ); 154 | $args = array( 155 | 'label' => $this->post_type_slug, 156 | 'description' => 'Stream', 157 | 'labels' => $labels, 158 | 'supports' => array( 'title' ), 159 | 'hierarchical' => false, 160 | 'public' => false, 161 | 'show_ui' => true, 162 | 'show_in_menu' => true, 163 | 'show_in_nav_menus' => false, 164 | 'show_in_admin_bar' => false, 165 | 'menu_position' => 5, 166 | 'menu_icon' => 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PGRlZnM+PHN0eWxlPi5he2ZpbGw6I2ZmZjt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPnN0cmVhbV9tYW5hZ2VyLWljb25fMTZ4MTYtd2hpdGU8L3RpdGxlPjxwYXRoIGNsYXNzPSJhIiBkPSJNOSwxMGExLDEsMCwxLDEtMiwwQTMuMjUsMy4yNSwwLDAsMSw4LDguMzMsMy4zLDMuMywwLDAsMSw5LDEwWiIvPjxwYXRoIGNsYXNzPSJhIiBkPSJNOCw0LjE3UzQuNSw3LjQ1LDQuNSwxMGEzLjUsMy41LDAsMSwwLDcsMEMxMS41LDcuNDYsOCw0LjE3LDgsNC4xN1pNOCwxMmEyLDIsMCwwLDEtMi0yQzYsOC41NSw4LDYuNjcsOCw2LjY3UzEwLDguNTUsMTAsMTBBMiwyLDAsMCwxLDgsMTJaIi8+PHBhdGggY2xhc3M9ImEiIGQ9Ik04LDBTMiw1LjYzLDIsMTBhNiw2LDAsMSwwLDEyLDBDMTQsNS42NSw4LDAsOCwwWk04LDE0LjVBNC41LDQuNSwwLDAsMSwzLjUsMTBDMy41LDYuNzMsOCwyLjUsOCwyLjVzNC41LDQuMjQsNC41LDcuNUE0LjQ5LDQuNDksMCwwLDEsOCwxNC41WiIvPjwvc3ZnPg==', 167 | 'can_export' => true, 168 | 'has_archive' => false, 169 | 'exclude_from_search' => true, 170 | 'publicly_queryable' => true, 171 | 'capability_type' => 'post', 172 | ); 173 | register_post_type( $this->post_type_slug, $args ); 174 | } 175 | 176 | /** 177 | * Add Stream post type messages. 178 | * 179 | * @since 1.0.0 180 | */ 181 | function define_post_type_messages($messages) { 182 | global $post, $post_ID; 183 | $post_type = get_post_type( $post_ID ); 184 | 185 | $obj = get_post_type_object($post_type); 186 | $singular = $obj->labels->singular_name; 187 | 188 | $messages[$this->post_type_slug] = array( 189 | 0 => '', 190 | 1 => __($singular . ' updated.'), 191 | 2 => __('Custom field updated.'), 192 | 3 => __('Custom field deleted.'), 193 | 4 => __($singular . ' updated.'), 194 | 5 => isset($_GET['revision']) ? sprintf( __($singular.' restored to revision from %s'), wp_post_revision_title( (int) $_GET['revision'], false ) ) : false, 195 | 6 => __($singular . ' published.'), 196 | 7 => __('Page saved.'), 197 | 8 => __($singular . ' submitted.'), 198 | 9 => sprintf( __($singular.' scheduled for: %1$s.'), date_i18n( __( 'M j, Y @ G:i' ) ) ), 199 | 10 => __($singular . ' draft updated.'), 200 | ); 201 | return $messages; 202 | } 203 | 204 | /** 205 | * Retrieve all streams from the database. 206 | * 207 | * @since 1.0.0 208 | * 209 | * @return array Collection of TimberStream objects 210 | */ 211 | public function get_streams( $query = array(), $PostClass = 'TimberStream' ) { 212 | if ($this->streams) return $this->streams; 213 | $query = array_merge( $query, array( 214 | 'post_type' => $this->post_type_slug, 215 | 'nopaging' => true 216 | )); 217 | return $this->streams = Timber::get_posts( $query, $PostClass ); 218 | } 219 | 220 | function add_timber_filters_functions($twig) { 221 | $twig->addFunction(new Twig_SimpleFunction('TimberStream', function ($pid, $StreamClass = 'TimberStream') { 222 | if (is_array($pid) && !TimberHelper::is_array_assoc($pid)) { 223 | foreach ($pid as &$p) { 224 | $p = new $StreamClass($p); 225 | } 226 | return $pid; 227 | } 228 | return new $StreamClass($pid); 229 | })); 230 | return $twig; 231 | } 232 | 233 | } 234 | -------------------------------------------------------------------------------- /includes/timber-stream.php: -------------------------------------------------------------------------------- 1 | $stream = new TimberStream( $pid ); 13 | * > foreach ( $stream->get_posts() as $post ) { 14 | * > echo ( $post->title ); 15 | * > } 16 | */ 17 | 18 | class TimberStream extends TimberPost { 19 | 20 | /** 21 | * Stream post cache. 22 | * 23 | * This will only be populated when TimberStream::get_posts 24 | * is run without a $query argument. 25 | * 26 | * @since 1.0.0 27 | * 28 | * @var array 29 | */ 30 | public $posts; 31 | 32 | /** 33 | * @since 1.0.0 34 | * @var array 35 | */ 36 | public $sm_query = array(); 37 | 38 | /** 39 | * Default stream options, used when creating a 40 | * new stream. 41 | * 42 | * @since 1.0.0 43 | * @var array 44 | */ 45 | public $default_options = array( 46 | 'query' => array( 47 | 'post_type' => 'post', 48 | 'post_status' => 'publish', 49 | 'has_password' => false, 50 | 'ignore_sticky_posts' => true, 51 | 'posts_per_page' => 100, 52 | 'orderby' => 'post__in' 53 | ), 54 | 55 | 'stream' => array(), 56 | 'layouts' => array( 57 | 'active' => 'default', 58 | 'layouts' => array( 59 | 'default' => array( 60 | 'name' => 'Default', 61 | 'zones' => array() 62 | ) 63 | ) 64 | ) 65 | ); 66 | 67 | /** 68 | * Stream options. 69 | * This is set by __construct based on what is stored 70 | * in the database. 71 | * 72 | * @since 1.0.0 73 | * 74 | * @var array 75 | */ 76 | public $options = null; 77 | 78 | /** 79 | * Construct Timber\Post, kick off stream init 80 | * 81 | * @param integer|boolean|string $pid Post ID or slug 82 | * 83 | * @todo allow creating a TimberStream w/out database 84 | */ 85 | public function __construct($pid = null) { 86 | parent::__construct($pid); 87 | $this->init_stream($pid); 88 | } 89 | 90 | /** 91 | * Init Stream object 92 | * 93 | * @param integer|boolean|string $pid Post ID or slug 94 | * 95 | */ 96 | public function init_stream($pid) { 97 | if ($this->post_type === 'sm_stream') { 98 | if ( !$this->post_content ) $this->post_content = serialize(array()); 99 | $this->options = array_merge( $this->default_options, unserialize($this->post_content) ); 100 | $this->options['query'] = apply_filters('stream-manager/query', $this->options['query']); 101 | $this->options = apply_filters( 'stream-manager/options/id=' . $this->ID, $this->options, $this ); 102 | $this->options = apply_filters( 'stream-manager/options/'.$this->slug, $this->options, $this ); 103 | 104 | $taxes = apply_filters( 'stream-manager/taxonomy/'.$this->slug, array(), $this ); 105 | if (is_array($taxes) && !empty($taxes)) { 106 | $taxes = StreamManagerUtilities::build_tax_query($taxes); 107 | if (isset($this->options['query']['tax_query'])) { 108 | $this->options['query']['tax_query'] = array_merge($this->options['query']['tax_query'], $taxes); 109 | } else { 110 | $this->options['query']['tax_query'] = $taxes; 111 | } 112 | } 113 | } 114 | 115 | } 116 | 117 | /** 118 | * Get filtered & sorted collection of posts in the stream 119 | * 120 | * @since 1.0.0 121 | * 122 | * @param array $query WP_Query query argument 123 | * @param string $PostClass Timber post class 124 | * 125 | * @return array collection of TimberPost objects 126 | */ 127 | public function get_posts($query = array(), $PostClass = 'TimberPost') { 128 | $cache = ( empty($query) || !is_array($query) ) ? true : false; 129 | 130 | if ( $cache && !empty($this->posts) ) return $this->posts; 131 | 132 | // Create an array of just post IDs 133 | $query = array_merge( $this->get('query'), $query ); 134 | $query['post__in'] = array(); 135 | foreach ( $this->get('stream') as $item ) { 136 | $query['post__in'][] = $item['id']; 137 | } 138 | if( isset( $query['post__not_in'] ) && is_array( $query['post__not_in'] ) ){ 139 | $query['post__in'] = array_diff( $query['post__in'], $query['post__not_in'] ); 140 | unset( $query['post__not_in'] ); 141 | } 142 | 143 | $posts_orig = Timber::get_posts($query, $PostClass); 144 | $post_ids = array_map(function($post) { 145 | return $post->ID; 146 | }, $posts_orig); 147 | 148 | //get posts that have been added via search that fall outside the tax rules 149 | $saved_posts = $this->get_posts_without_tax_query($query); 150 | $extra = array_diff($saved_posts, $post_ids); 151 | $all_ids = array_merge($extra,$post_ids); 152 | 153 | //use the stream to put posts back in order 154 | foreach($this->get('stream') as $item) { 155 | if(in_array($item['id'], $all_ids)) { 156 | $posts[] = new $PostClass($item['id']); 157 | } 158 | } 159 | 160 | if (empty($posts)) { 161 | // if the user has re-configured the feed we might need to blow out the saved items to make way for the fresh query; 162 | unset($query['post__in']); 163 | $posts = Timber::get_posts($query, $PostClass); 164 | } 165 | $pinned = array_keys($this->filter_stream('pinned', true)); 166 | 167 | foreach ($posts as &$post) { 168 | $post->pinned = in_array( $post->ID, $pinned ); 169 | } 170 | 171 | if ( $cache ) $this->posts = $posts; 172 | 173 | return $posts; 174 | } 175 | 176 | /** 177 | * Get the ids of all saved posts, including any removed by the taxonomy query 178 | * 179 | */ 180 | public function get_posts_without_tax_query($query, $PostClass = 'TimberPost') { 181 | 182 | // Remove any taxonomy limitations, since those would remove any 183 | // posts from the stream that were added by searching in the UI. 184 | unset($query['tax_query']); 185 | 186 | $all_posts = Timber::get_posts($query, $PostClass); 187 | $postids = array_map(function($post) { 188 | return $post->ID; 189 | }, $all_posts); 190 | 191 | return $postids; 192 | 193 | } 194 | 195 | /** 196 | * Filter posts in the stream, returning only the filtered 197 | * posts (including their position). 198 | * 199 | * @since 1.0.0 200 | * 201 | * @return array filtered posts 202 | */ 203 | public function filter_stream($attribute, $value) { 204 | $items = array(); 205 | 206 | foreach ( $this->get('stream') as $position => $item ) { 207 | $item['position'] = $position; 208 | if ( $item[$attribute] == $value ) $items[$item['id']] = $item; 209 | } 210 | 211 | return $items; 212 | } 213 | 214 | 215 | /** 216 | * Enforce the stream length. 217 | * 218 | * If there are fewer posts than allowed, add some from the base query. 219 | * If there are more, remove them. 220 | * 221 | * @since 1.0.0 222 | * 223 | * @todo it's possible for a pinned item to go above the limit 224 | */ 225 | public function repopulate_stream() { 226 | 227 | // Determine how many over/under we are 228 | $query = $this->get('query'); 229 | $difference = count( $this->get('stream') ) - $query['posts_per_page']; 230 | 231 | if ( $difference < 0 ) { 232 | 233 | // Under -- add pinned posts to the end 234 | $query = $this->get('query'); 235 | $ids = array(); 236 | foreach ( $this->get('stream') as $post ) { 237 | $ids[] = $post['id']; 238 | } 239 | $query['post__not_in'] = $ids; 240 | $query['posts_per_page'] = $difference * -1; 241 | $posts = Timber::get_posts($query); 242 | 243 | $this->remove_pinned(); 244 | 245 | foreach ( $posts as $post ) { 246 | $this->options['stream'][] = array( 247 | 'id' => $post->ID, 248 | 'pinned' => false 249 | ); 250 | } 251 | 252 | $this->reinsert_pinned(); 253 | 254 | } else if ( $difference > 0 ) { 255 | 256 | // Over -- remove non-pinned posts at the end 257 | $this->remove_pinned(); 258 | for ( $i = 1; $i <= $difference; $i++ ) { 259 | array_pop( $this->options['stream'] ); 260 | } 261 | $this->reinsert_pinned(); 262 | 263 | } 264 | } 265 | 266 | 267 | /** 268 | * Checks if a post exists in a stream 269 | * 270 | * @since 1.0.0 271 | * 272 | * @param integer $post_id Post ID 273 | * 274 | * @return array returns the data saved in the stream, plus its position 275 | */ 276 | public function check_post ( $post_id ) { 277 | foreach ( $this->get('stream') as $position => $item ) { 278 | if ( $item['id'] == $post_id ) { 279 | $item['position'] = $position; 280 | return $item; 281 | } 282 | } 283 | return false; 284 | } 285 | 286 | /** 287 | * Removes a post from a stream and, by default, fills 288 | * in the empty space at the end. 289 | * 290 | * @since 1.0.0 291 | * 292 | * @param integer $post_id Post ID 293 | * @param boolean $repopulate add/remove posts to enforce stream length 294 | */ 295 | public function remove_post ( $post_id, $repopulate = true ) { 296 | $post = $this->check_post( $post_id ); 297 | if ( $post ) { 298 | $this->remove_pinned(); 299 | 300 | // Remove non-pinned 301 | unset($this->options['stream'][ $post['position'] ]); 302 | 303 | // Remove pinned 304 | foreach ( $this->pinned as $i => $pinned ) { 305 | if ( $pinned['id'] == $post_id ) { 306 | unset( $this->pinned[$i] ); 307 | } 308 | } 309 | $this->reinsert_pinned(); 310 | if ( $repopulate ) $this->repopulate_stream(); 311 | $this->save_stream(); 312 | } 313 | } 314 | 315 | /** 316 | * Inserts a post in the stream 317 | * 318 | * @since 1.0.0 319 | * 320 | * @param integer $post_id Post ID 321 | */ 322 | public function insert_post ( $post_id ) { 323 | // Does it already exist? If so, remove it, and we'll reinsert it 324 | if ( $this->check_post( $post_id ) ) { 325 | $this->remove_post( $post_id, false ); 326 | } 327 | 328 | // Determine where it is in the original query (if at all), 329 | // minus any pinned items 330 | $query = array_merge( $this->get('query'), array( 331 | 'post__not_in' => array_keys($this->filter_stream('pinned', true)) 332 | )); 333 | $posts = Timber::get_posts( $query ); 334 | 335 | $in_stream = false; 336 | 337 | foreach ( $posts as $i => $post ) { 338 | if ( $post->ID == $post_id ) $in_stream = $i; 339 | } 340 | 341 | // If it's not in the stream, bail 342 | if ( $in_stream === false ) return; 343 | 344 | // Remove pinned items from the stream... 345 | $this->remove_pinned(); 346 | 347 | // ... then insert this post ... 348 | array_splice( $this->options['stream'], $in_stream, 0, array( array ( 349 | 'id' => $post_id, 350 | 'pinned' => false 351 | ) ) ); 352 | 353 | // ... and then reinsert the pinned items 354 | $this->reinsert_pinned(); 355 | $this->repopulate_stream(); 356 | $this->save_stream(); 357 | } 358 | 359 | /** 360 | * Temporarily removes pinned items from the stream, for the 361 | * purpose of modifying the auto-flowing stream. 362 | * 363 | * @since 1.0.0 364 | */ 365 | public function remove_pinned() { 366 | $this->pinned = $this->filter_stream('pinned', true); 367 | foreach ( $this->pinned as $pin ) { 368 | unset ( $this->options['stream'][ $pin['position'] ] ); 369 | } 370 | } 371 | 372 | /** 373 | * Place the pinned items back in the stream in their appropriate 374 | * locations 375 | * 376 | * @since 1.0.0 377 | */ 378 | public function reinsert_pinned() { 379 | foreach ( $this->pinned as $pin ) { 380 | $position = $pin['position']; 381 | unset( $pin['position'] ); 382 | array_splice( $this->options['stream'], $position, 0, array( $pin ) ); 383 | } 384 | } 385 | 386 | 387 | public function get( $key ) { 388 | return apply_filters( 'stream-manager/get_option/id=' . $this->ID, $this->options[$key], $key, $this ); 389 | } 390 | 391 | public function set( $key, $value ) { 392 | $this->options[$key] = apply_filters( 'stream-manager/set_option/id=' . $this->ID, $value, $key, $this ); 393 | } 394 | 395 | 396 | 397 | /** 398 | * Save the stream metadata 399 | * 400 | * @since 1.0.0 401 | */ 402 | 403 | 404 | public function save_stream() { 405 | $save_data = apply_filters( 'stream-manager/save/id=' . $this->ID, array( 406 | 'ID' => $this->ID, 407 | 'post_content' => serialize($this->options) 408 | ), $this); 409 | 410 | // Fix conflict with yoast premium 411 | add_filter('wpseo_premium_post_redirect_slug_change', '__return_true'); 412 | 413 | wp_update_post( $save_data ); 414 | 415 | remove_filter('wpseo_premium_post_redirect_slug_change', '__return_true'); 416 | 417 | } 418 | 419 | } 420 | -------------------------------------------------------------------------------- /includes/views/add.twig: -------------------------------------------------------------------------------- 1 | 2 |
    3 |
    4 | 5 | 6 |
      7 |
      8 | 9 |
      -------------------------------------------------------------------------------- /includes/views/meta.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /includes/views/rules.twig: -------------------------------------------------------------------------------- 1 | 2 |
      3 | {{ function('post_categories_meta_box', post, null ) }} 4 |
      5 | 6 |
      7 | {{ function('post_tags_meta_box', post, null) }} 8 |
      9 | 10 | Reload Stream -------------------------------------------------------------------------------- /includes/views/stream.twig: -------------------------------------------------------------------------------- 1 |
      2 | 3 |
      4 | {% set j = 0 %} 5 | 6 | {% for i, post in posts %} 7 | 8 | {% for zone in layout.zones %} 9 | {% if zone.position == j %} 10 | {% include ["meta.twig", "views/meta.twig"] %} 11 | {% set j = j + 1 %} 12 | {% endif %} 13 | {% endfor %} 14 | 15 | {% include ["stub.twig", "views/stub.twig"] %} 16 | 17 | {% set j = j + 1 %} 18 | 19 | {% endfor %} 20 | 21 |
      22 | 23 | {{ nonce }} 24 | -------------------------------------------------------------------------------- /includes/views/stub.twig: -------------------------------------------------------------------------------- 1 |
      2 | 3 | 4 | 5 | 6 | 7 |
      8 | {% if post.thumbnail and post.thumbnail.src %} 9 | 10 | {% endif %} 11 | 12 | {{ post.title }} 13 | {# 14 |
      15 | {{post.get_preview(10, true, '')}} 16 |
      17 | #} 18 | 29 |
      30 | 31 | 32 | 36 |
      37 | -------------------------------------------------------------------------------- /includes/views/zones.twig: -------------------------------------------------------------------------------- 1 | 2 | {# 7 | 8 |

      + Add Layout

      9 |

      10 | 11 | 12 |

      13 | 14 |

      + Add Zone

      15 |

      #} 16 | 17 | 18 | {#

      #} 19 | 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stream-manager", 3 | "version": "1.3.4", 4 | "description": "WordPress plugin to curate streams of the latests posts.", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "tests" 8 | }, 9 | "devDependencies": { 10 | "gulp": "^3.9.1", 11 | "gulp-clean": "^0.3.2", 12 | "gulp-clean-css": "^3.3.1", 13 | "gulp-git": "^2.2.0", 14 | "gulp-load-plugins": "^1.5.0", 15 | "gulp-replace": "^0.5.4", 16 | "gulp-sass": "^3.1.0", 17 | "gulp-sequence": "^0.4.6", 18 | "gulp-svn": "^1.0.7", 19 | "gulp-uglify": "^2.1.2" 20 | }, 21 | "scripts": { 22 | "test": "echo \"Error: no test specified\" && exit 1" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/Upstatement/stream-manager.git" 27 | }, 28 | "author": "Upstatement", 29 | "license": "ISC", 30 | "bugs": { 31 | "url": "https://github.com/Upstatement/stream-manager/issues" 32 | }, 33 | "homepage": "https://github.com/Upstatement/stream-manager#readme" 34 | } 35 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | ./tests/ 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | includes 21 | stream-manager.php 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /stream-manager.php: -------------------------------------------------------------------------------- 1 |

      Please install Timber to use Stream Manager.

      '); 59 | }); 60 | return; 61 | } 62 | } 63 | 64 | 65 | 66 | //////////////////////////////////////////// 67 | // 68 | // Public-Facing Functionality 69 | // 70 | //////////////////////////////////////////// 71 | 72 | require_once( plugin_dir_path( __FILE__ ) . 'includes/class-stream-manager-utilities.php' ); 73 | require_once( plugin_dir_path( __FILE__ ) . 'includes/class-stream-manager-ajax-helper.php' ); 74 | require_once( plugin_dir_path( __FILE__ ) . 'includes/class-stream-manager.php' ); 75 | require_once( plugin_dir_path( __FILE__ ) . 'includes/timber-stream.php' ); 76 | require_once( plugin_dir_path( __FILE__ ) . 'includes/class-stream-manager-manager.php'); 77 | require_once( plugin_dir_path( __FILE__ ) . 'includes/class-stream-manager-api.php'); 78 | 79 | 80 | 81 | 82 | add_action( 'plugins_loaded', array( 'StreamManager', 'get_instance' ) ); 83 | add_action( 'plugins_loaded', array( 'StreamManagerManager', 'get_instance' ) ); 84 | 85 | 86 | //////////////////////////////////////////// 87 | // 88 | // Dashboard & Administrative Functionality 89 | // 90 | //////////////////////////////////////////// 91 | 92 | if ( is_admin() ) { 93 | require_once( plugin_dir_path( __FILE__ ) . 'includes/class-stream-manager-admin.php' ); 94 | 95 | add_action( 'plugins_loaded', array( 'StreamManagerAdmin', 'get_instance' ) ); 96 | } 97 | 98 | 99 | -------------------------------------------------------------------------------- /tests/StreamManager_UnitTestCase.php: -------------------------------------------------------------------------------- 1 | factory->post->create(array('post_date' => $date)); 10 | } 11 | return $post_ids; 12 | } 13 | 14 | function buildStream( $name = 'Sample Stream', $options = array() ) { 15 | $pid = $this->factory->post->create(array('post_type' => 'sm_stream', 'post_content' => '', 'post_title' => $name)); 16 | add_filter('stream-manager/options/id='.$pid, function($defaults, $stream) use ($options) { 17 | $defaults['query'] = array_merge($defaults['query'], $options); 18 | return $defaults; 19 | }, 10, 2); 20 | $stream = new TimberStream($pid); 21 | return $stream; 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | assertObjectHasAttribute('plugin', $admin); 8 | $this->assertObjectHasAttribute('default_query', $admin); 9 | } 10 | 11 | function testSaveStream() { 12 | $admin = StreamManagerAdmin::get_instance(); 13 | $stream = $this->buildStream(); 14 | $postids = $this->buildPosts(5); 15 | foreach($postids as $id) { 16 | $_POST['sm_sort'][] = $id; 17 | } 18 | $admin->save_stream($stream->ID, false); 19 | $stream = new TimberStream($stream->ID); 20 | $this->assertEquals(5, count($stream->options['stream'])); 21 | } 22 | 23 | function testAddHelpText() { 24 | set_current_screen( 'sm_stream' ); 25 | $admin = StreamManagerAdmin::get_instance(); 26 | $admin->add_help_text(); 27 | $screen = get_current_screen(); 28 | $tabs = $screen->get_help_tabs(); 29 | $this->assertEquals(3, count($tabs)); 30 | } 31 | 32 | function testDefinePostTypes() { 33 | $manager = StreamManager::get_instance(); 34 | $manager->define_post_types(); 35 | $post_types = get_post_types(); 36 | $this->assertTrue(in_array($manager->get_post_type_slug(), $post_types)); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /tests/test-stream-manager-ajax-helper.php: -------------------------------------------------------------------------------- 1 | factory->post->create(); 7 | $queue = array(array('id' => $postid, 'position' => 0)); 8 | $output = StreamManagerAjaxHelper::retrieve_posts($queue); 9 | $this->assertEquals($output[$postid]['position'], 0); 10 | $html = $output[$postid]['object']; 11 | if (strpos($html, 'id="post-'.$postid) !== false) { 12 | $contains_id = true; 13 | } else { 14 | $contains_id = false; 15 | } 16 | $this->assertTrue($contains_id); 17 | } 18 | 19 | function testRetrievePostsCheckTitle() { 20 | $postid = $this->factory->post->create(array('post_title' => 'The Wrong Trousers')); 21 | $queue = array(array('id' => $postid, 'position' => 0)); 22 | $output = StreamManagerAjaxHelper::retrieve_posts($queue); 23 | $html = $output[$postid]['object']; 24 | if (strpos($html, 'The Wrong Trousers') !== false) { 25 | $contains_title = true; 26 | } else { 27 | $contains_title = false; 28 | } 29 | $this->assertTrue($contains_title); 30 | } 31 | 32 | function testSearchPosts() { 33 | $pid = $this->buildStream(); 34 | $bagel_post = $this->factory->post->create(array('post_title' => 'Bagels')); 35 | $croissant_post = $this->factory->post->create(array('post_title' => 'Croissants')); 36 | $pastries_post = $this->factory->post->create(array('post_title' => 'Croissants and Bagels')); 37 | $output = StreamManagerAjaxHelper::search_posts('bagel', $pid); 38 | $this->assertEquals(2, count($output)); 39 | $this->assertEquals('1 min', $output[0]['human_date']); 40 | } 41 | 42 | function testSearchPostsAppliesFilter() { 43 | $pid = $this->buildStream('Test Stream', array('post_type' => 'pastry')); 44 | $this->factory->post->create(array('post_title' => 'bagel1')); 45 | $this->factory->post->create(array('post_title' => 'bagel2')); 46 | $this->factory->post->create(array('post_title' => 'bagel3', 'post_type' => 'pastry')); 47 | $output = StreamManagerAjaxHelper::search_posts('bagel', $pid); 48 | $this->assertEquals(1, count($output)); 49 | } 50 | 51 | function testSearchPostsNoMatches() { 52 | $pid = $this->buildStream(); 53 | $bagel_post = $this->factory->post->create(array('post_title' => 'Bagels')); 54 | $output = StreamManagerAjaxHelper::search_posts('muffin', $pid); 55 | $this->assertEquals(0, count($output)); 56 | } 57 | 58 | } -------------------------------------------------------------------------------- /tests/test-stream-manager-api.php: -------------------------------------------------------------------------------- 1 | assertTrue($exists); 9 | } 10 | 11 | function testStreamExistsFalse() { 12 | $exists = StreamManagerApi::stream_exists( 'test_stream' ); 13 | $this->assertFalse($exists); 14 | } 15 | 16 | function testInsertStream() { 17 | $sid = StreamManagerApi::insert_stream( 'test_stream' ); 18 | $streams = get_posts( array( 'post_type' => 'sm_stream' ) ); 19 | $this->assertEquals( 1, count( $streams ) ); 20 | $this->assertEquals( $streams[0]->post_name, 'test_stream'); 21 | } 22 | 23 | function testInsertStreamIfExists() { 24 | $sid = StreamManagerApi::insert_stream( 'test_stream' ); 25 | $sid = StreamManagerApi::insert_stream( 'test_stream' ); 26 | $streams = get_posts( array( 'post_type' => 'sm_stream' ) ); 27 | $this->assertEquals( 1, count( $streams ) ); 28 | $this->assertEquals( $streams[0]->post_name, 'test_stream'); 29 | } 30 | 31 | function testInsertStreamWithFilter() { 32 | $cid = wp_create_category('local'); 33 | $cat = get_category( $cid ); 34 | $sid = StreamManagerApi::insert_stream('local', null, array('category_name' => $cat->name ) ); 35 | $postid = $this->factory->post->create( array( 'post_category' => array( $cid ) ) ); 36 | $postid2 = $this->factory->post->create(); 37 | $all_posts = get_posts(); 38 | $this->assertEquals( 2, count( $all_posts ) ); 39 | $stream = new TimberStream( $sid ); 40 | $posts = $stream->get_posts(); 41 | $this->assertEquals( 1, count( $posts ) ); 42 | } 43 | 44 | function testDeleteStream() { 45 | $sid = StreamManagerApi::insert_stream('test_stream'); 46 | $streams = get_posts( array( 'post_type' => 'sm_stream' ) ); 47 | $stream = $streams[0]; 48 | $this->assertEquals( 1, count( $streams ) ); 49 | $deleted = StreamManagerApi::delete_stream( $stream->post_name ); 50 | $streams = get_posts( array( 'post_type' => 'sm_stream' ) ); 51 | $this->assertEquals( 0, count( $streams ) ); 52 | $this->assertEquals($sid, $deleted); 53 | } 54 | 55 | function testDeleteStreamIfDoesntExist() { 56 | $deleted = StreamManagerApi::delete_stream( 'test' ); 57 | $this->assertFalse($deleted); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /tests/test-stream-manager-hooks.php: -------------------------------------------------------------------------------- 1 | buildStream('Sample Stream', array('post_type' => 'article')); 7 | $this->buildPosts(5); 8 | $posts = $stream->get_posts(); 9 | $this->assertEquals( 0, count($posts) ); 10 | } 11 | 12 | function testEmptyTaxonomyQueryHook() { 13 | $cooking_id = $this->factory->term->create(array('name' => 'Cooking'.rand(0, 1000))); 14 | $stream = $this->buildStream('Sample Stream', array('post_type' => 'post', 'tax_query' => array('relation' => 'OR', array('taxonomy' => 'post_tag', 'field' => 'term_id', 'terms' => array($cooking_id))))); 15 | $this->buildPosts(5); 16 | $posts = $stream->get_posts(); 17 | $this->assertEquals( 0, count($posts) ); 18 | } 19 | 20 | function testSinglePostTaxonomyQueryHook() { 21 | $cooking_id = $this->factory->term->create(array('name' => 'Cooking')); 22 | $stream = $this->buildStream('Sample Stream', array('post_type' => 'post', 'tax_query' => array('relation' => 'OR', array('taxonomy' => 'post_tag', 'field' => 'term_id', 'terms' => array($cooking_id))))); 23 | $this->buildPosts(5); 24 | $cooking_post = $this->factory->post->create(array('tags_input' => 'cooking', 'post_title' => 'All About Cooking')); 25 | $posts = $stream->get_posts(); 26 | $this->assertEquals( 1, count($posts) ); 27 | $this->assertEquals( $cooking_post, $posts[0]->ID ); 28 | } 29 | 30 | function testDoublePostTaxonomyQueryHook() { 31 | $jump_term_id = $this->factory->term->create(array('name' => 'Jumping')); 32 | $jive_term_id = $this->factory->term->create(array('name' => 'Jiveing')); 33 | $stream = $this->buildStream('Sample Stream', array('post_type' => 'post', 'tax_query' => array('relation' => 'OR', array('taxonomy' => 'post_tag', 'field' => 'term_id', 'terms' => array($jump_term_id, $jive_term_id))))); 34 | $this->buildPosts(5); 35 | $jumping_post = $this->factory->post->create(array('tags_input' => 'jumping', 'post_title' => 'Jumping & Jiving', 'post_date' => '2014-12-15 12:00:00')); 36 | $jiving_post = $this->factory->post->create(array('tags_input' => 'jumping', 'post_title' => 'Jiving n Jumping', 'post_date' => '2014-12-25 12:00:00')); 37 | $posts = $stream->get_posts(); 38 | $this->assertEquals( 2, count($posts) ); 39 | $this->assertEquals( $jiving_post, $posts[0]->ID ); 40 | } 41 | 42 | function testSinglePostTaxonomyHook() { 43 | //build some stuff 44 | $baking_id = $this->factory->term->create(array('name' => 'Baking')); 45 | $sid = $this->factory->post->create(array('post_type' => 'sm_stream', 'post_content' => '', 'post_title' => 'Sample Stream')); 46 | $stream = new TimberPost($sid); 47 | 48 | //add a filter 49 | add_filter('stream-manager/taxonomy/'.$stream->slug, function($defaults) use ($baking_id) { 50 | $defaults['relation'] = "OR"; 51 | $defaults['post_tag'] = array( $baking_id ); 52 | return $defaults; 53 | }); 54 | $stream = new TimberStream($sid); 55 | 56 | //now make some posts 57 | $baking_post = $this->factory->post->create(array('tags_input' => 'baking', 'post_title' => 'Cookies')); 58 | $this->buildPosts(5); 59 | 60 | //and get them 61 | $posts = $stream->get_posts(); 62 | $this->assertEquals( 1, count($posts) ); 63 | } 64 | 65 | function testSinglePostDoubleHooks() { 66 | $handstand_id = $this->factory->term->create( array('name' => 'Handstands') ); 67 | add_filter('stream-manager/taxonomy/fitness-stream', function($defaults) use ($handstand_id) { 68 | $defaults['relation'] = "OR"; 69 | $defaults['post_tag'] = array( $handstand_id ); 70 | return $defaults; 71 | }); 72 | 73 | $parkour_id = $this->factory->term->create( array('name' => 'Parkour') ); 74 | 75 | $stream = $this->buildStream('Fitness Stream', array('post_type' => 'post', 'tax_query' => array('relation' => 'OR', array('taxonomy' => 'post_tag', 'field' => 'term_id', 'terms' => array($parkour_id))))); 76 | 77 | 78 | $parkour_post = $this->factory->post->create(array('tags_input' => 'parkour', 'post_title' => 'Parkour!')); 79 | $handstand_post = $this->factory->post->create(array('tags_input' => 'handstands', 'post_title' => 'Handstands')); 80 | $combo_post = $this->factory->post->create(array('tags_input' => 'parkour, handstands', 'post_title' => 'Parkour & Handstands')); 81 | $combo = new TimberPost($combo_post); 82 | $this->buildPosts(5); 83 | $posts = $stream->get_posts(); 84 | $this->assertEquals( 3, count($posts) ); 85 | } 86 | 87 | 88 | 89 | } 90 | -------------------------------------------------------------------------------- /tests/test-stream-manager-integration.php: -------------------------------------------------------------------------------- 1 | buildPosts(10); 7 | $this->assertEquals(10, count($pids)); 8 | $pids = $this->buildPosts(7); 9 | $this->assertEquals(7, count($pids)); 10 | } 11 | 12 | function testBasicStream() { 13 | $count = rand(0, 25); 14 | $this->buildPosts($count); 15 | $stream = $this->buildStream(); 16 | $posts = $stream->get_posts(); 17 | $this->assertEquals($count, count($posts)); 18 | } 19 | 20 | function testPostPublish() { 21 | $stream = $this->buildStream(); 22 | $this->buildPosts(4); 23 | $posts = $stream->get_posts(); 24 | $this->assertEquals(4, count($posts)); 25 | $post_id = $this->factory->post->create(array('post_status' => 'draft')); 26 | wp_publish_post($post_id); 27 | $posts = $stream->get_posts(array('post_type' => 'post')); 28 | $this->assertEquals(5, count($posts)); 29 | } 30 | 31 | function testPostUnpublish() { 32 | $stream = $this->buildStream(); 33 | $post_id = $this->factory->post->create(); 34 | $posts = $stream->get_posts(); 35 | $this->assertEquals(1, count($posts)); 36 | wp_update_post(array( 'ID' => $post_id, 'post_status' => 'draft' )); 37 | $stream = new TimberStream( $stream->ID ); 38 | $posts = $stream->get_posts(); 39 | $this->assertEquals(0, count($posts)); 40 | } 41 | 42 | function testRemovePost() { 43 | $stream = $this->buildStream(); 44 | $postids = $this->buildPosts(5); 45 | foreach($postids as $id) { 46 | $data[] = array('id' => $id, 'pinned' => ''); 47 | } 48 | $stream->set('stream', $data); 49 | $posts = $stream->get_posts(); 50 | $first = $posts[0]->ID; 51 | $stream->remove_post($first); 52 | $stream = new TimberStream($stream->ID); 53 | $posts = $stream->get_posts(); 54 | $this->assertEquals($first, $posts[4]->ID); 55 | } 56 | 57 | function testRemovePinnedPost() { 58 | $stream = $this->buildStream(); 59 | $postids = $this->buildPosts(5); 60 | foreach($postids as $id) { 61 | $data[] = array('id' => $id, 'pinned' => 1); 62 | } 63 | $stream->set('stream', $data); 64 | $posts = $stream->get_posts(); 65 | $first = $posts[0]->ID; 66 | $stream->remove_post($first); 67 | $stream = new TimberStream($stream->ID); 68 | $posts = $stream->get_posts(); 69 | $this->assertEquals($first, $posts[4]->ID); 70 | } 71 | 72 | } 73 | --------------------------------------------------------------------------------