mirror of
https://github.com/pattern-lab/patternlab-php.git
synced 2025-01-17 06:08:23 +01:00
initial commit for the new repo
This commit is contained in:
parent
a76c24c465
commit
ddd5602233
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
.DS_Store
|
||||
public/index.html
|
||||
public/styleguide.html
|
||||
public/patterns/a*
|
||||
public/patterns/m*
|
||||
public/patterns/o*
|
||||
public/patterns/p*
|
||||
config.ini
|
||||
latest-change.txt
|
||||
/public/styleguide/js/styleguide-ck.js
|
||||
/public/listeners/synclisteners-ck.js
|
172
README.md
172
README.md
@ -1,4 +1,170 @@
|
||||
patternlab-php
|
||||
==============
|
||||
# Pattern Lab - PHP Library
|
||||
|
||||
## About Pattern Lab - The Idea
|
||||
|
||||
Brad had a crazy idea.
|
||||
|
||||
## About Pattern Lab - The Tech
|
||||
|
||||
From a technical perspective, Pattern Lab is, at its core, a static site generator. When generating a site, the Pattern Lab Builder takes material from the `source/` directory (e.g. templates & data), compiles them, and writes them out to the `public/` directory. The Builder auto-generates the drop-down nav, the "View All" links & content, as well as the Style Guide. In "watch" mode the Builder will also monitor the main styles file (or any files you list in the config) and move it as appropriate.
|
||||
|
||||
By making it a static site generator, Pattern Lab strongly separates views/data from build logic from presentation. So anyone can come in and build a Builder, the middleware, in their language of choice or, hell, rewrite it (and move some stuff around) and try to make it all front-end only. The compiled presentation bits can be moved anywhere (e.g. FTP to a public website) without concern for back-end tech.
|
||||
|
||||
## Installation
|
||||
|
||||
After downloading Pattern Lab you can do the following to set it up:
|
||||
|
||||
### 1. Configure Apache
|
||||
|
||||
Most of the features in Pattern Lab require it to be running on a web server like Apache. The ideal set-up is to run Apache and Pattern Lab locally on your computer. If you don't know how to set-up Apache there are directions for Mac OS X in `extras/apache/`. At the very least, the `DocumentRoot` for the site should be set-to `/path/to/patternlab/public/`.
|
||||
|
||||
### 2. Configure & Pre-Build Pattern Lab
|
||||
|
||||
By default, a number of important pages, including the main page, *aren't* built when you first download Pattern Lab. Before you visit your install of Pattern Lab you'll need to make sure all of the necessary pages have been built.
|
||||
|
||||
To generate your site do the following:
|
||||
|
||||
1. Open `scripts/`
|
||||
2. Double-click `generateSite.command`
|
||||
|
||||
The site should now be generated and available. Simply follow the "Regular Use" steps to finish the set-up process.
|
||||
|
||||
## Regular Use Workflow
|
||||
|
||||
**THIS IS DAVE'S EXPECTED USE CASE AND IT CAN BE MORE NUANCED THAN THIS**
|
||||
|
||||
A fairly normal workflow with Pattern Lab will go like this:
|
||||
|
||||
1. You open Pattern Lab to edit patterns
|
||||
2. Double-click on `scripts/watchForChanges.command` to generate the site and have the Builder watch for any future changes and auto-compile them
|
||||
3. Make your changes in `source/patterns`
|
||||
4. Refresh your browser
|
||||
5. Go to step 3.
|
||||
|
||||
If you want to add JavaScript, images or CSS you can place them in `public/`.
|
||||
|
||||
## Pattern Lab Extended Options
|
||||
|
||||
Their are several options you can enable when using Pattern Lab on a regular basis to smooth out your workflow.
|
||||
|
||||
1. Patterns can be automatically regenerated after you save them,
|
||||
2. Your browser can automatically reload when, again, files are automatically saved, and,
|
||||
3. When testing, you can set-up browsers to follow the browsing history of another.
|
||||
|
||||
### Watch for Changes & Auto-Regenerate Patterns
|
||||
|
||||
Once you've generated the site Pattern Lab can watch for changes to patterns or their related data. When they're being watched, Pattern Lab's public files will automagically be rendered when you save patterns or their related data. To see the changes all you need to do is refresh your browser.
|
||||
|
||||
To set-up the watch do the following:
|
||||
|
||||
1. Open `scripts/`
|
||||
2. Double-click `startAutoRegenerate.command`
|
||||
|
||||
To make Pattern Lab stop watching your files just press`CTRL+C`.
|
||||
|
||||
By default, Pattern Lab will monitor the `pattern.mustache` and `data.json` files in `source/patterns`. It will also watch `source/data/data.json` as well as any user-defined files listed in `config/config.ini`. For example, you might want to track a Sass-built `styles.css` file.
|
||||
|
||||
### Auto-Reload the Browser Window When Content Updates
|
||||
|
||||
Rather than manually refreshing your browser you can have Pattern Lab auto-reload your browser window for you. To turn this feature on do the following:
|
||||
|
||||
1. Open `scripts/`
|
||||
2. Double-click `startAutoReloadServer.command`
|
||||
3. Refresh the Pattern Lab site
|
||||
|
||||
Your browser should now be listening for auto-reload events. The Pattern Lab toolbar should note that content sync is now "on." For this feature to work you *must* have enabled the "Watch for Changes" feature previously discussed.
|
||||
|
||||
**Please note:** If you find that content sync is not working properly please make sure your browser [supports WebSockets](http://caniuse.com/websockets).
|
||||
|
||||
### Sync Pattern Browsing Across Multiple Tabs or Browsers (aka Page Follow)
|
||||
|
||||
If you want to test a pattern in multiple tabs or browsers without refreshing them all or having to navigate to new patterns in each simply use Pattern Lab's page follow feature. Any browser or tab should control all of the browsers or tabs. To turn this feature on do the following:
|
||||
|
||||
1. Open `scripts/`
|
||||
2. Double-click `startPageFollowServer.command`
|
||||
3. Refresh the Pattern Lab site
|
||||
|
||||
Your browser should now be listening for page follow events. The Pattern Lab toolbar should note that page follow is now "on." Any other browser that visits the Pattern Lab site should now be redirected to the last visited pattern. When one browser views another pattern they should all be updated.
|
||||
|
||||
If you want to link patterns together (for a demo or to flip between "page" patterns) you can use the following format to have Pattern Lab put in the correct path:
|
||||
|
||||
<a href="{{ link.full-pattern-name }}">Link Text</a>
|
||||
|
||||
For example, to link to the block hero molecule pattern you would use:
|
||||
|
||||
<a href="{{ link.m-blocks-block-hero }}">Link Text</a>
|
||||
|
||||
If you want to view patterns on your mobile device simply do the following:
|
||||
|
||||
1. Make sure your mobile device and computer are on the same WiFi network
|
||||
2. Note the IP address for your computer (found under System Preferences > Sharing)
|
||||
3. Replace the star with your IP address in the following address: `patternlab.*.xip.io`
|
||||
4. Enter that into the browser on your mobile device
|
||||
|
||||
The above assumes that your Apache VirtualHost has `patternlab.*.xip.io` (with the star, not your IP address) as a `ServerAlias`. If it doesn't please add it.
|
||||
|
||||
**Please note:** If you find that page follow is not working properly please make sure your browser [supports WebSockets](http://caniuse.com/websockets).
|
||||
|
||||
## All About Patterns
|
||||
|
||||
All you ever wanted to know about patterns.
|
||||
|
||||
### How Patterns Are Organized
|
||||
|
||||
Patterns are organized into atoms, molecules, organisms, and pages. The pattern directories have the following naming convention:
|
||||
|
||||
[patternComplexity]-[patternType]-[patternName]
|
||||
|
||||
This is what each means:
|
||||
|
||||
* `patternComplexity` denotes the overall type of pattern. _a_ is for atoms, _m_ is for molecules, _o_ is for organisms, and _p_ is for pages.
|
||||
* `patternType` denotes the sub-type of pattern. This helps to organize patterns in the drop downs in Pattern Lab.
|
||||
* `patternName` is obviously the actual name of the pattern.
|
||||
|
||||
In order for Pattern Lab to work you must follow the directory naming convention when creating new patterns. Also, a pattern **must** be named `pattern.mustache`.
|
||||
|
||||
### Modifying Individual Patterns
|
||||
|
||||
To modify an individual pattern simply open it up and edit it. Patterns can be found in `source/[pattern-name]/pattern.mustache`. Pattern Lab supports the [Mustache syntax](http://mustache.github.io/mustache.5.html).
|
||||
|
||||
### Including One Pattern Within Another
|
||||
|
||||
To use another pattern in another, for example to create a molecule from several atoms, just use the [Mustache](http://mustache.github.io/mustache.5.html) partials syntax. The name of the partial should be the name of the directory for the partial you want to include. For example, to include the logo image atom you'd use:
|
||||
|
||||
{{> a-images-logo }}
|
||||
|
||||
### Adding/Modifying Static Assets for a Pattern
|
||||
|
||||
To add static assets like a special CSS file that's included by your pattern or an image that's referenced by a pattern simply put the asset in the appropriate directory in `public/`. For example, JavaScript should go in `public/js/` and images should go in `public/images/`.
|
||||
|
||||
**Please note:** In the case of CSS you may want to store those files in the `source/` directory and track them with the Pattern Lab's Builder Watch feature. This way, as you make changes to them your browser can auto-reload. To add files to be tracked by the builder simply edit `config/config.ini`.
|
||||
|
||||
### Modifying Data for a Pattern
|
||||
|
||||
A pattern can reference variables, [via Mustache](http://mustache.github.io/mustache.5.html), to include "dynamic" content. Depending on your preferences you can define and/or modify data in three places:
|
||||
|
||||
1. Include a `data.json` file in the pattern directory itself. When referencing the data in the pattern you **must** scope the data to that pattern's name. For example, if you want to reference the data in the `data.json` in the `source/patterns/a-images-landscape-4x3` directory in your pattern you must reference `{{ a-images-landscape-4x3.src }}`.
|
||||
2. Modify `source/data/data.json` and use a nested naming scheme. For example, the first entry in the default file nests by type and sub-type. To reference the nested landscape 4x3 image in your pattern you'd use `{{ atoms.images.landscape-4x3.src }}`.
|
||||
3. Modify `source/data/data.json` and use a flat naming scheme. For example, the second entry in the default file doesn't nest the object at all. To reference the flat landscape 4x3 image in your pattern you'd use `{{ landscape-4x3.src }}`
|
||||
|
||||
All of these are supported "out-of-the-box." There's no need to settle on any particular format.
|
||||
|
||||
## Credits
|
||||
|
||||
The default install of Pattern Lab uses a number of PHP libraries. They are:
|
||||
|
||||
* [mustache.php](https://github.com/bobthecow/mustache.php)
|
||||
* [Wrench](https://github.com/varspool/Wrench)
|
||||
|
||||
## IDEAS
|
||||
|
||||
Ok, so these are the things I want to work on cleaning up:
|
||||
|
||||
* <del>I've done a simple tweak where choosing an option updates the iframe instead of reloading the page. I want to get the accordion working properly and, more importantly, I want changes made to patterns, data & styles reflected in an auto-update to the existing window. So you could "update a pattern," system generates new mark-up, the viewer re-loads the iframe and updates the pattern nav w/ a helpful "we've been updated text/color change." Hopefully this would make for a clean, iterative process.</del>
|
||||
* <del>add a way to easily reference other templates as part of a pattern. this would add the "click-through" feature.</del>
|
||||
* not sure about the versioning... I can see a method for it but it makes PHP an absolute requirement in the short-term.
|
||||
* <del>that said, i'd also love to make this multi-device capable (e.g. update pattern on a desktop and see it show up across multiple devices). not sure how ish works in that context but it'd be cool to play around with. this would also require a server-side language choice... i think.</del>
|
||||
* this repo has to be cleaned & moved to a github organization. you have at least one image which shouldn't be here and, since it's under version control, it'll always be here even in a delete.
|
||||
|
||||
Crazy idea, a codepen like interface for modifying the pattern, related data, and styles in the browser. I don't think I could touch related JS.
|
||||
|
||||
The PHP version of Pattern Lab
|
||||
|
74
builder/builder.php
Normal file
74
builder/builder.php
Normal file
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
/*!
|
||||
* Pattern Lab Builder CLI - v0.1
|
||||
*
|
||||
* Copyright (c) 2013 Dave Olsen, http://dmolsen.com
|
||||
* Licensed under the MIT license
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* php builder.php -g
|
||||
* Iterates over the 'source' directories & files and generates the entire site a single time.
|
||||
*
|
||||
* php builder.php -w
|
||||
* Generates the site like the -g flag and then watches for changes in the 'source' directories &
|
||||
* files. Will re-generate files if they've changed.
|
||||
*
|
||||
*/
|
||||
|
||||
// load builder classes
|
||||
require __DIR__."/lib/builder.lib.php";
|
||||
require __DIR__."/lib/generator.lib.php";
|
||||
require __DIR__."/lib/watcher.lib.php";
|
||||
|
||||
// load mustache & register it
|
||||
require __DIR__."/lib/mustache/Autoloader.php";
|
||||
Mustache_Autoloader::register();
|
||||
|
||||
// make sure this script is being accessed from the command line
|
||||
if (php_sapi_name() == 'cli') {
|
||||
|
||||
$args = getopt("gw");
|
||||
|
||||
if (isset($args["g"])) {
|
||||
|
||||
// initiate the g (generate) switch
|
||||
|
||||
// iterate over the source directory and generate the site
|
||||
$g = new Generator();
|
||||
$g->generate();
|
||||
print "your site has been generated...\n";
|
||||
|
||||
} elseif (isset($args["w"])) {
|
||||
|
||||
// initiate the w (watch) switch
|
||||
|
||||
// iterate over the source directory and generate the site
|
||||
$g = new Generator();
|
||||
$g->generate();
|
||||
print "your site has been generated...\n";
|
||||
|
||||
// watch the source directory and regenerate any changed files
|
||||
$w = new Watcher();
|
||||
print "watching your site for changes...\n";
|
||||
$w->watch();
|
||||
|
||||
} else {
|
||||
|
||||
// when in doubt write out the usage
|
||||
print "\n";
|
||||
print "Usage:\n\n";
|
||||
print " php builder.php -g\n";
|
||||
print " Iterates over the 'source' directories & files and generates the entire site a single time.\n\n";
|
||||
print " php builder.php -w\n";
|
||||
print " Generates the site like the -g flag and then watches for changes in the 'source' directories &\n";
|
||||
print " files. Will re-generate files if they've changed.\n\n";
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
print "The builder script can only be run from the command line.";
|
||||
|
||||
}
|
455
builder/lib/builder.lib.php
Normal file
455
builder/lib/builder.lib.php
Normal file
@ -0,0 +1,455 @@
|
||||
<?php
|
||||
|
||||
/*!
|
||||
* Pattern Lab Builder Class - v0.1
|
||||
*
|
||||
* Copyright (c) 2013 Dave Olsen, http://dmolsen.com
|
||||
* Licensed under the MIT license
|
||||
*
|
||||
*/
|
||||
|
||||
class Builder {
|
||||
|
||||
// i was lazy when i started this project & kept (mainly) to two letter vars. sorry.
|
||||
protected $m; // mustache instance
|
||||
protected $d; // data from data.json files
|
||||
protected $sp; // source patterns dir
|
||||
protected $pp; // public patterns dir
|
||||
protected $dp; // permissions for the public pattern dirs
|
||||
protected $fp; // permissions for the public pattern files
|
||||
protected $if; // directories/files to be ignored in source/patterns
|
||||
protected $wf; // files to be watched to see if they should be moved
|
||||
protected $mf; // where the files should be moved too
|
||||
protected $websocketAddress; // for populating the websockets template partial
|
||||
protected $contentSyncPort; // for populating the websockets template partial
|
||||
protected $navSyncPort; // for populating the websockets template partial
|
||||
|
||||
/**
|
||||
* When initializing the Builder class or the sub-classes make sure the base properties are configured
|
||||
* Also, create the config if it doesn't already exist
|
||||
*/
|
||||
public function __construct() {
|
||||
|
||||
// set-up the configuration options for patternlab
|
||||
if (!($config = @parse_ini_file(__DIR__."/../../config/config.ini"))) {
|
||||
// config.ini didn't exist so attempt to create it using the default file
|
||||
if (!@copy(__DIR__."/../../config/config.ini.default", __DIR__."/../../config/config.ini")) {
|
||||
print "Please make sure config.ini.default exists before trying to have Pattern Lab build the config.ini file automagically.";
|
||||
exit;
|
||||
} else {
|
||||
$config = parse_ini_file(__DIR__."/../../config/config.ini");
|
||||
}
|
||||
}
|
||||
|
||||
// populate some standard variables out of the config
|
||||
foreach ($config as $key => $value) {
|
||||
|
||||
// if the variables are array-like make sure the properties are validated/trimmed before saving
|
||||
if (($key == "if") || ($key == "wf") || ($key == "mf")) {
|
||||
$values = explode(",",$value);
|
||||
array_walk($values,'Builder::trim');
|
||||
$this->$key = $values;
|
||||
} else {
|
||||
$this->$key = $value;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Simply returns a new Mustache instance
|
||||
*
|
||||
* @return {Object} an instance of the Mustache engine
|
||||
*/
|
||||
protected function mustacheInstance() {
|
||||
return new Mustache_Engine(array(
|
||||
'loader' => new Mustache_Loader_PatternLoader(__DIR__.$this->sp),
|
||||
"partials_loader" => new Mustache_Loader_PatternLoader(__DIR__.$this->sp)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a pattern within the context of spitting out a finished pattern w/ header & footer
|
||||
* @param {String} the filename of the file to be rendered
|
||||
* @param {Object} the instance of mustache to be used in the rendering
|
||||
*
|
||||
* @return {String} the final rendered pattern including the standard header and footer for a pattern
|
||||
*/
|
||||
private function renderFile($f,$m) {
|
||||
$h = file_get_contents(__DIR__.$this->sp."d-wrapper/header.html");
|
||||
$rf = $this->renderPattern($f,$m);
|
||||
$f = file_get_contents(__DIR__.$this->sp."d-wrapper/footer.html");
|
||||
return $h."\n".$rf."\n".$f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a given pattern file using Mustache and incorporating the provided data
|
||||
* @param {String} the filename of the file to be rendered
|
||||
* @param {Object} the instance of mustache to be used in the rendering
|
||||
*
|
||||
* @return {String} the mark-up as rendered by Mustache
|
||||
*/
|
||||
protected function renderPattern($f,$m) {
|
||||
return $m->render($f,$this->d);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates a mustache instance, renders out a full pattern file and places it in the public directory
|
||||
*
|
||||
* @return {String} the mark-up placed in it's appropriate location in the public directory
|
||||
*/
|
||||
protected function renderAndMove() {
|
||||
|
||||
// initiate a mustache instance
|
||||
$m = $this->mustacheInstance();
|
||||
|
||||
// scan the pattern source directory
|
||||
foreach(glob(__DIR__.$this->sp."*/*.mustache") as $filename) {
|
||||
|
||||
// render the file
|
||||
$entry = $this->getEntry($filename,"m");
|
||||
$r = $this->renderFile($entry."/".$entry.".mustache",$m);
|
||||
|
||||
// if the pattern directory doesn't exist create it
|
||||
if (!is_dir(__DIR__.$this->pp.$entry)) {
|
||||
mkdir(__DIR__.$this->pp.$entry);
|
||||
//chmod($this->pp.$entry,$this->dp);
|
||||
file_put_contents(__DIR__.$this->pp.$entry."/pattern.html",$r);
|
||||
//chmod($this->pp.$entry."/pattern.html",$this->fp);
|
||||
} else {
|
||||
file_put_contents(__DIR__.$this->pp.$entry."/pattern.html",$r);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the index page and style guide
|
||||
*
|
||||
* @return {String} writes out the index and style guides
|
||||
*/
|
||||
protected function generateMainPages() {
|
||||
|
||||
// render out the main pages and move them to public
|
||||
$nd = $this->gatherNavItems();
|
||||
$nd['contentsyncport'] = $this->contentSyncPort;
|
||||
$nd['navsyncport'] = $this->navSyncPort;
|
||||
|
||||
// grab the partials into a data object for the style guide
|
||||
$sd = $this->gatherPartials();
|
||||
|
||||
// render the "view all" pages
|
||||
$this->generateViewAllPages();
|
||||
|
||||
// render the index page and the style guide
|
||||
$e = new Mustache_Engine(array(
|
||||
'loader' => new Mustache_Loader_FilesystemLoader(__DIR__."/../../source/templates/"),
|
||||
'partials_loader' => new Mustache_Loader_FilesystemLoader(__DIR__."/../../source/templates/partials/"),
|
||||
));
|
||||
$r = $e->render('index',$nd);
|
||||
file_put_contents(__DIR__."/../../public/index.html",$r);
|
||||
|
||||
$s = $e->render('styleguide',$sd);
|
||||
file_put_contents(__DIR__."/../../public/styleguide.html",$s);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the view all pages
|
||||
*
|
||||
* @return {String} writes out each view all page
|
||||
*/
|
||||
protected function generateViewAllPages() {
|
||||
|
||||
// silly to do this again but makes sense in light of the fact that watcher needs to use this function too
|
||||
$nd = $this->gatherNavItems();
|
||||
|
||||
// add view all to each list
|
||||
$i = 0; $k = 0;
|
||||
foreach ($nd['buckets'] as $bucket) {
|
||||
|
||||
foreach ($bucket["navItems"] as $navItem) {
|
||||
|
||||
foreach ($navItem["navSubItems"] as $subItem) {
|
||||
if ($subItem["patternName"] == "View All") {
|
||||
|
||||
// get all the rendered partials that match
|
||||
$sid = $this->gatherPartialsByMatch($subItem["patternPath"]);
|
||||
|
||||
// render the viewall template
|
||||
$e = new Mustache_Engine(array(
|
||||
'loader' => new Mustache_Loader_FilesystemLoader(__DIR__."/../../source/templates/"),
|
||||
'partials_loader' => new Mustache_Loader_FilesystemLoader(__DIR__."/../../source/templates/partials/"),
|
||||
));
|
||||
$v = $e->render('viewall',$sid);
|
||||
|
||||
// if the pattern directory doesn't exist create it
|
||||
if (!is_dir(__DIR__.$this->pp.$subItem["patternPath"])) {
|
||||
mkdir(__DIR__.$this->pp.$subItem["patternPath"]);
|
||||
//chmod($this->pp.$entry,$this->dp);
|
||||
file_put_contents(__DIR__.$this->pp.$subItem["patternPath"]."/pattern.html",$v);
|
||||
//chmod($this->pp.$entry."/pattern.html",$this->fp);
|
||||
} else {
|
||||
file_put_contents(__DIR__.$this->pp.$subItem["patternPath"]."/pattern.html",$v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$i++;
|
||||
$k = 0;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather data from source/data/data.json and data.json files in pattern directories
|
||||
* Throws all the data into the Builder class scoped d var
|
||||
*/
|
||||
protected function gatherData() {
|
||||
|
||||
// gather the data from the main source data.json
|
||||
if (file_exists(__DIR__."/../../source/data/data.json")) {
|
||||
$this->d = (object) array_merge(array(), (array) json_decode(file_get_contents(__DIR__."/../../source/data/data.json")));
|
||||
}
|
||||
|
||||
// this makes link a reserved word but oh well...
|
||||
$this->d->link = new stdClass();
|
||||
|
||||
// gather data from pattern/data.json
|
||||
foreach(glob(__DIR__.$this->sp."*/data.json") as $filename) {
|
||||
|
||||
$entry = $this->getEntry($filename,"j");
|
||||
$this->d->link->$entry = "/patterns/".$entry."/pattern.html";
|
||||
|
||||
if (file_exists(__DIR__.$this->sp.$entry."/data.json")) {
|
||||
$d = new stdClass();
|
||||
$d->$entry = json_decode(file_get_contents(__DIR__.$this->sp.$entry."/data.json"));
|
||||
$this->d = (object) array_merge((array) $this->d, (array) $d);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Gathers the partials for the nav drop down in Pattern Lab
|
||||
*
|
||||
* @return {Array} the nav items organized by type
|
||||
*/
|
||||
protected function gatherNavItems() {
|
||||
|
||||
$b = array(); // the array that will contain the items
|
||||
$t = array(); // the array that will contain the english names for the types of buckets
|
||||
$cc = ""; // current class of the object we're looking at (e.g. atom)
|
||||
$cn = 0; // track the number for the array
|
||||
$sc = ""; // current sub-class of the object we're looking at (e.g. block)
|
||||
$sn = 0; // track the number for the array
|
||||
$n = ""; // the name of the final object
|
||||
|
||||
$b["buckets"] = array();
|
||||
$t = array("a" => "Atoms", "m" => "Molecules", "o" => "Organisms", "p" => "Pages");
|
||||
$cco = $cc; // prepopulate the "old" check of the previous current class
|
||||
$cno = $cn; // prepopulate the "old" check of the previous current class
|
||||
$sco = $sc; // prepopulate the "old" check of the previous current class
|
||||
$sno = $sn;
|
||||
|
||||
// scan the pattern source directory
|
||||
$entries = scandir(__DIR__."/".$this->sp);
|
||||
foreach($entries as $entry) {
|
||||
|
||||
// decide which files in the source directory might need to be ignored
|
||||
if (!in_array($entry,$this->if) && ($entry[0] != '_')) {
|
||||
$els = explode("-",$entry,3);
|
||||
$cc = $els[0];
|
||||
$sc = $els[1];
|
||||
$n = ucwords(str_replace("-"," ",$els[2]));
|
||||
|
||||
// place items in their buckets. i'm already confused looking back at this. it works tho...
|
||||
if ($cc == $cco) {
|
||||
if ($sc == $sco) {
|
||||
$b["buckets"][$cno]["navItems"][$sno]["navSubItems"][] = array(
|
||||
"patternPath" => $entry,
|
||||
"patternName" => $n
|
||||
);
|
||||
} else {
|
||||
$sn++;
|
||||
$b["buckets"][$cno]["navItems"][$sn] = array(
|
||||
"sectionNameLC" => $sc,
|
||||
"sectionNameUC" => ucwords($sc),
|
||||
"navSubItems" => array(
|
||||
array(
|
||||
"patternPath" => $entry,
|
||||
"patternName" => $n
|
||||
)));
|
||||
$sco = $sc;
|
||||
$sno = $sn;
|
||||
}
|
||||
} else {
|
||||
$b["buckets"][$cn] = array(
|
||||
"bucketNameLC" => strtolower($t[$cc]),
|
||||
"bucketNameUC" => $t[$cc],
|
||||
"navItems" => array(
|
||||
array(
|
||||
"sectionNameLC" => $sc,
|
||||
"sectionNameUC" => ucwords($sc),
|
||||
"navSubItems" => array(
|
||||
array(
|
||||
"patternPath" => $entry,
|
||||
"patternName" => $n
|
||||
)))));
|
||||
$cco = $cc;
|
||||
$sco = $sc;
|
||||
$cno = $cn;
|
||||
$cn++;
|
||||
$sn = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add view all to each list
|
||||
$i = 0; $k = 0;
|
||||
foreach ($b['buckets'] as $bucket) {
|
||||
|
||||
if ($bucket["bucketNameLC"] != "pages") {
|
||||
foreach ($bucket["navItems"] as $navItem) {
|
||||
|
||||
$subItemsCount = count($navItem["navSubItems"]);
|
||||
$pathItems = explode("-",$navItem["navSubItems"][0]["patternPath"]);
|
||||
if (count($pathItems) > 0) {
|
||||
$viewAll = array("patternPath" => $pathItems[0]."-".$pathItems[1], "patternName" => "View All");
|
||||
$b['buckets'][$i]["navItems"][$k]["navSubItems"][$subItemsCount] = $viewAll;
|
||||
}
|
||||
|
||||
$k++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$i++;
|
||||
$k = 0;
|
||||
}
|
||||
|
||||
return $b;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the patterns in the source directory so they can be used in the default styleguide
|
||||
*
|
||||
* @return {Array} an array of rendered partials
|
||||
*/
|
||||
protected function gatherPartials() {
|
||||
|
||||
$m = $this->mustacheInstance();
|
||||
$p = array("partials" => array());
|
||||
|
||||
// scan the pattern source directory
|
||||
foreach(glob(__DIR__.$this->sp."*/*.mustache") as $filename) {
|
||||
|
||||
$entry = $this->getEntry($filename,"m");
|
||||
|
||||
// make sure 'pages' get ignored. templates will have to be added to the ignore as well
|
||||
if ($entry[0] != "p") {
|
||||
|
||||
if (file_exists(__DIR__."/".$this->sp.$entry."/".$entry.".mustache")) {
|
||||
|
||||
// render the partial and stick it in the array
|
||||
$p["partials"][] = $this->renderPattern($entry."/".$entry.".mustache",$m);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $p;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the patterns that match a given string so they can be used in the view all styleguides
|
||||
* It's duplicative but I'm tired
|
||||
*
|
||||
* @return {Array} an array of rendered partials that match the given path
|
||||
*/
|
||||
protected function gatherPartialsByMatch($pathMatch) {
|
||||
|
||||
$m = $this->mustacheInstance();
|
||||
$p = array("partials" => array());
|
||||
|
||||
// scan the pattern source directory
|
||||
foreach(glob(__DIR__.$this->sp."*/*.mustache") as $filename) {
|
||||
|
||||
$entry = $this->getEntry($filename,"m");
|
||||
|
||||
// decide which files in the source directory might need to be ignored
|
||||
if (($entry[0] != "p") && strstr($entry,$pathMatch)) {
|
||||
if (file_exists(__DIR__."/".$this->sp.$entry."/".$entry.".mustache")) {
|
||||
|
||||
// render the partial and stick it in the array
|
||||
$p["partials"][] = $this->renderPattern($entry."/".$entry.".mustache",$m);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $p;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the directory for a given pattern by parsing the file path
|
||||
* @param {String} the filepath for a directory that contained the match
|
||||
* @param {String} the the type of match for the pattern matching
|
||||
*
|
||||
* @return {String} the directory for the pattern
|
||||
*/
|
||||
protected function getEntry($filepath,$type) {
|
||||
$file = ($type == 'm') ? '(.*)\.mustache' : 'data\.json';
|
||||
if (preg_match('/\/(([amotp])\-([A-z0-9]{1,})\-([A-z0-9-]{1,}))\/'.$file.'$/',$filepath,$matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write out the time tracking file so the content sync service will work. A holdover
|
||||
* from how I put together the original AJAX polling set-up.
|
||||
*
|
||||
* @return {String} file containing a timestamp
|
||||
*/
|
||||
protected function updateChangeTime() {
|
||||
|
||||
if (is_dir(__DIR__."/../../public/")) {
|
||||
file_put_contents(__DIR__."/../../public/latest-change.txt",time());
|
||||
} else {
|
||||
print "Either the public directory for Pattern Lab doesn't exist or the builder is in the wrong location. Please fix.";
|
||||
exit;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Print out the data var. For debugging purposes
|
||||
*
|
||||
* @return {String} the formatted version of the d object
|
||||
*/
|
||||
public function printData() {
|
||||
print_r($this->d);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trim a given string. Used in the array_walk() function in __construct as a sanity check
|
||||
* @param {String} an entry from one of the list-based config entries
|
||||
*
|
||||
* @return {String} trimmed version of the given $v var
|
||||
*/
|
||||
public function trim(&$v) {
|
||||
$v = trim($v);
|
||||
}
|
||||
|
||||
}
|
46
builder/lib/generator.lib.php
Normal file
46
builder/lib/generator.lib.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
/*!
|
||||
* Pattern Lab Generator Class - v0.1
|
||||
*
|
||||
* Copyright (c) 2013 Dave Olsen, http://dmolsen.com
|
||||
* Licensed under the MIT license
|
||||
*
|
||||
* Compiles and moves all files in the source/patterns dir to public/patterns dir.
|
||||
* Vast majority of logic is in builder.lib.php
|
||||
*
|
||||
*/
|
||||
|
||||
class Generator extends Builder {
|
||||
|
||||
/**
|
||||
* Use the Builder __construct to gather the config variables
|
||||
*/
|
||||
public function __construct() {
|
||||
|
||||
// construct the parent
|
||||
parent::__construct();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Main logic. Gathers data, gets partials, and generates patterns
|
||||
* Also generates the main index file and styleguide
|
||||
*/
|
||||
public function generate() {
|
||||
|
||||
// gather data
|
||||
$this->gatherData();
|
||||
|
||||
// render out the patterns and move them to public/patterns
|
||||
$this->renderAndMove();
|
||||
|
||||
// render out the index and style guide
|
||||
$this->generateMainPages();
|
||||
|
||||
// update the change time so the auto-reload will fire (doesn't work for the index and style guide)
|
||||
$this->updateChangeTime();
|
||||
|
||||
}
|
||||
|
||||
}
|
69
builder/lib/mustache/Autoloader.php
Normal file
69
builder/lib/mustache/Autoloader.php
Normal file
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Mustache.php.
|
||||
*
|
||||
* (c) 2012 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Mustache class autoloader.
|
||||
*/
|
||||
class Mustache_Autoloader
|
||||
{
|
||||
|
||||
private $baseDir;
|
||||
|
||||
/**
|
||||
* Autoloader constructor.
|
||||
*
|
||||
* @param string $baseDir Mustache library base directory (default: dirname(__FILE__).'/..')
|
||||
*/
|
||||
public function __construct($baseDir = null)
|
||||
{
|
||||
if ($baseDir === null) {
|
||||
$this->baseDir = dirname(__FILE__).'/..';
|
||||
} else {
|
||||
$this->baseDir = rtrim($baseDir, '/');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new instance as an SPL autoloader.
|
||||
*
|
||||
* @param string $baseDir Mustache library base directory (default: dirname(__FILE__).'/..')
|
||||
*
|
||||
* @return Mustache_Autoloader Registered Autoloader instance
|
||||
*/
|
||||
public static function register($baseDir = null)
|
||||
{
|
||||
$loader = new self($baseDir);
|
||||
spl_autoload_register(array($loader, 'autoload'));
|
||||
|
||||
return $loader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Autoload Mustache classes.
|
||||
*
|
||||
* @param string $class
|
||||
*/
|
||||
public function autoload($class)
|
||||
{
|
||||
if ($class[0] === '\\') {
|
||||
$class = substr($class, 1);
|
||||
}
|
||||
|
||||
if (strpos($class, 'Mustache') !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$file = sprintf('%s/%s.php', $this->baseDir, str_replace('_', '/', $class));
|
||||
if (is_file($file)) {
|
||||
require $file;
|
||||
}
|
||||
}
|
||||
}
|
475
builder/lib/mustache/Compiler.php
Normal file
475
builder/lib/mustache/Compiler.php
Normal file
@ -0,0 +1,475 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Mustache.php.
|
||||
*
|
||||
* (c) 2012 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Mustache Compiler class.
|
||||
*
|
||||
* This class is responsible for turning a Mustache token parse tree into normal PHP source code.
|
||||
*/
|
||||
class Mustache_Compiler
|
||||
{
|
||||
|
||||
private $sections;
|
||||
private $source;
|
||||
private $indentNextLine;
|
||||
private $customEscape;
|
||||
private $charset;
|
||||
private $strictCallables;
|
||||
private $pragmas;
|
||||
|
||||
/**
|
||||
* Compile a Mustache token parse tree into PHP source code.
|
||||
*
|
||||
* @param string $source Mustache Template source code
|
||||
* @param string $tree Parse tree of Mustache tokens
|
||||
* @param string $name Mustache Template class name
|
||||
* @param bool $customEscape (default: false)
|
||||
* @param string $charset (default: 'UTF-8')
|
||||
* @param bool $strictCallables (default: false)
|
||||
*
|
||||
* @return string Generated PHP source code
|
||||
*/
|
||||
public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8', $strictCallables = false)
|
||||
{
|
||||
$this->pragmas = array();
|
||||
$this->sections = array();
|
||||
$this->source = $source;
|
||||
$this->indentNextLine = true;
|
||||
$this->customEscape = $customEscape;
|
||||
$this->charset = $charset;
|
||||
$this->strictCallables = $strictCallables;
|
||||
|
||||
return $this->writeCode($tree, $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for walking the Mustache token parse tree.
|
||||
*
|
||||
* @throws Mustache_Exception_SyntaxException upon encountering unknown token types.
|
||||
*
|
||||
* @param array $tree Parse tree of Mustache tokens
|
||||
* @param int $level (default: 0)
|
||||
*
|
||||
* @return string Generated PHP source code
|
||||
*/
|
||||
private function walk(array $tree, $level = 0)
|
||||
{
|
||||
$code = '';
|
||||
$level++;
|
||||
foreach ($tree as $node) {
|
||||
switch ($node[Mustache_Tokenizer::TYPE]) {
|
||||
case Mustache_Tokenizer::T_PRAGMA:
|
||||
$this->pragmas[$node[Mustache_Tokenizer::NAME]] = true;
|
||||
break;
|
||||
|
||||
case Mustache_Tokenizer::T_SECTION:
|
||||
$code .= $this->section(
|
||||
$node[Mustache_Tokenizer::NODES],
|
||||
$node[Mustache_Tokenizer::NAME],
|
||||
$node[Mustache_Tokenizer::INDEX],
|
||||
$node[Mustache_Tokenizer::END],
|
||||
$node[Mustache_Tokenizer::OTAG],
|
||||
$node[Mustache_Tokenizer::CTAG],
|
||||
$level
|
||||
);
|
||||
break;
|
||||
|
||||
case Mustache_Tokenizer::T_INVERTED:
|
||||
$code .= $this->invertedSection(
|
||||
$node[Mustache_Tokenizer::NODES],
|
||||
$node[Mustache_Tokenizer::NAME],
|
||||
$level
|
||||
);
|
||||
break;
|
||||
|
||||
case Mustache_Tokenizer::T_PARTIAL:
|
||||
case Mustache_Tokenizer::T_PARTIAL_2:
|
||||
$code .= $this->partial(
|
||||
$node[Mustache_Tokenizer::NAME],
|
||||
isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '',
|
||||
$level
|
||||
);
|
||||
break;
|
||||
|
||||
case Mustache_Tokenizer::T_UNESCAPED:
|
||||
case Mustache_Tokenizer::T_UNESCAPED_2:
|
||||
$code .= $this->variable($node[Mustache_Tokenizer::NAME], false, $level);
|
||||
break;
|
||||
|
||||
case Mustache_Tokenizer::T_COMMENT:
|
||||
break;
|
||||
|
||||
case Mustache_Tokenizer::T_ESCAPED:
|
||||
$code .= $this->variable($node[Mustache_Tokenizer::NAME], true, $level);
|
||||
break;
|
||||
|
||||
case Mustache_Tokenizer::T_TEXT:
|
||||
$code .= $this->text($node[Mustache_Tokenizer::VALUE], $level);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Mustache_Exception_SyntaxException(sprintf('Unknown token type: %s', $node[Mustache_Tokenizer::TYPE]), $node);
|
||||
}
|
||||
}
|
||||
|
||||
return $code;
|
||||
}
|
||||
|
||||
const KLASS = '<?php
|
||||
|
||||
class %s extends Mustache_Template
|
||||
{
|
||||
private $lambdaHelper;%s
|
||||
|
||||
public function renderInternal(Mustache_Context $context, $indent = \'\')
|
||||
{
|
||||
$this->lambdaHelper = new Mustache_LambdaHelper($this->mustache, $context);
|
||||
$buffer = \'\';
|
||||
%s
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
%s
|
||||
}';
|
||||
|
||||
const KLASS_NO_LAMBDAS = '<?php
|
||||
|
||||
class %s extends Mustache_Template
|
||||
{%s
|
||||
public function renderInternal(Mustache_Context $context, $indent = \'\')
|
||||
{
|
||||
$buffer = \'\';
|
||||
%s
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
}';
|
||||
|
||||
const STRICT_CALLABLE = 'protected $strictCallables = true;';
|
||||
|
||||
/**
|
||||
* Generate Mustache Template class PHP source.
|
||||
*
|
||||
* @param array $tree Parse tree of Mustache tokens
|
||||
* @param string $name Mustache Template class name
|
||||
*
|
||||
* @return string Generated PHP source code
|
||||
*/
|
||||
private function writeCode($tree, $name)
|
||||
{
|
||||
$code = $this->walk($tree);
|
||||
$sections = implode("\n", $this->sections);
|
||||
$klass = empty($this->sections) ? self::KLASS_NO_LAMBDAS : self::KLASS;
|
||||
$callable = $this->strictCallables ? $this->prepare(self::STRICT_CALLABLE) : '';
|
||||
|
||||
return sprintf($this->prepare($klass, 0, false, true), $name, $callable, $code, $sections);
|
||||
}
|
||||
|
||||
const SECTION_CALL = '
|
||||
// %s section
|
||||
$buffer .= $this->section%s($context, $indent, $context->%s(%s));
|
||||
';
|
||||
|
||||
const SECTION = '
|
||||
private function section%s(Mustache_Context $context, $indent, $value)
|
||||
{
|
||||
$buffer = \'\';
|
||||
if (%s) {
|
||||
$source = %s;
|
||||
$buffer .= $this->mustache
|
||||
->loadLambda((string) call_user_func($value, $source, $this->lambdaHelper)%s)
|
||||
->renderInternal($context, $indent);
|
||||
} elseif (!empty($value)) {
|
||||
$values = $this->isIterable($value) ? $value : array($value);
|
||||
foreach ($values as $value) {
|
||||
$context->push($value);%s
|
||||
$context->pop();
|
||||
}
|
||||
}
|
||||
|
||||
return $buffer;
|
||||
}';
|
||||
|
||||
/**
|
||||
* Generate Mustache Template section PHP source.
|
||||
*
|
||||
* @param array $nodes Array of child tokens
|
||||
* @param string $id Section name
|
||||
* @param int $start Section start offset
|
||||
* @param int $end Section end offset
|
||||
* @param string $otag Current Mustache opening tag
|
||||
* @param string $ctag Current Mustache closing tag
|
||||
* @param int $level
|
||||
*
|
||||
* @return string Generated section PHP source code
|
||||
*/
|
||||
private function section($nodes, $id, $start, $end, $otag, $ctag, $level)
|
||||
{
|
||||
$method = $this->getFindMethod($id);
|
||||
$id = var_export($id, true);
|
||||
$source = var_export(substr($this->source, $start, $end - $start), true);
|
||||
$callable = $this->getCallable();
|
||||
|
||||
if ($otag !== '{{' || $ctag !== '}}') {
|
||||
$delims = ', '.var_export(sprintf('{{= %s %s =}}', $otag, $ctag), true);
|
||||
} else {
|
||||
$delims = '';
|
||||
}
|
||||
|
||||
$key = ucfirst(md5($delims."\n".$source));
|
||||
|
||||
if (!isset($this->sections[$key])) {
|
||||
$this->sections[$key] = sprintf($this->prepare(self::SECTION), $key, $callable, $source, $delims, $this->walk($nodes, 2));
|
||||
}
|
||||
|
||||
return sprintf($this->prepare(self::SECTION_CALL, $level), $id, $key, $method, $id);
|
||||
}
|
||||
|
||||
const INVERTED_SECTION = '
|
||||
// %s inverted section
|
||||
$value = $context->%s(%s);
|
||||
if (empty($value)) {
|
||||
%s
|
||||
}';
|
||||
|
||||
/**
|
||||
* Generate Mustache Template inverted section PHP source.
|
||||
*
|
||||
* @param array $nodes Array of child tokens
|
||||
* @param string $id Section name
|
||||
* @param int $level
|
||||
*
|
||||
* @return string Generated inverted section PHP source code
|
||||
*/
|
||||
private function invertedSection($nodes, $id, $level)
|
||||
{
|
||||
$method = $this->getFindMethod($id);
|
||||
$id = var_export($id, true);
|
||||
|
||||
return sprintf($this->prepare(self::INVERTED_SECTION, $level), $id, $method, $id, $this->walk($nodes, $level));
|
||||
}
|
||||
|
||||
const PARTIAL = '
|
||||
if ($partial = $this->mustache->loadPartial(%s)) {
|
||||
$buffer .= $partial->renderInternal($context, %s);
|
||||
}
|
||||
';
|
||||
|
||||
/**
|
||||
* Generate Mustache Template partial call PHP source.
|
||||
*
|
||||
* @param string $id Partial name
|
||||
* @param string $indent Whitespace indent to apply to partial
|
||||
* @param int $level
|
||||
*
|
||||
* @return string Generated partial call PHP source code
|
||||
*/
|
||||
private function partial($id, $indent, $level)
|
||||
{
|
||||
return sprintf(
|
||||
$this->prepare(self::PARTIAL, $level),
|
||||
var_export($id, true),
|
||||
var_export($indent, true)
|
||||
);
|
||||
}
|
||||
|
||||
const VARIABLE = '
|
||||
$value = $this->resolveValue($context->%s(%s), $context, $indent);%s
|
||||
$buffer .= %s%s;
|
||||
';
|
||||
|
||||
/**
|
||||
* Generate Mustache Template variable interpolation PHP source.
|
||||
*
|
||||
* @param string $id Variable name
|
||||
* @param boolean $escape Escape the variable value for output?
|
||||
* @param int $level
|
||||
*
|
||||
* @return string Generated variable interpolation PHP source
|
||||
*/
|
||||
private function variable($id, $escape, $level)
|
||||
{
|
||||
$filters = '';
|
||||
|
||||
if (isset($this->pragmas[Mustache_Engine::PRAGMA_FILTERS])) {
|
||||
list($id, $filters) = $this->getFilters($id, $level);
|
||||
}
|
||||
|
||||
$method = $this->getFindMethod($id);
|
||||
$id = ($method !== 'last') ? var_export($id, true) : '';
|
||||
$value = $escape ? $this->getEscape() : '$value';
|
||||
|
||||
return sprintf($this->prepare(self::VARIABLE, $level), $method, $id, $filters, $this->flushIndent(), $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Mustache Template variable filtering PHP source.
|
||||
*
|
||||
* @param string $id Variable name
|
||||
* @param int $level
|
||||
*
|
||||
* @return string Generated variable filtering PHP source
|
||||
*/
|
||||
private function getFilters($id, $level)
|
||||
{
|
||||
$filters = array_map('trim', explode('|', $id));
|
||||
$id = array_shift($filters);
|
||||
|
||||
return array($id, $this->getFilter($filters, $level));
|
||||
}
|
||||
|
||||
const FILTER = '
|
||||
$filter = $context->%s(%s);
|
||||
if (!(%s)) {
|
||||
throw new Mustache_Exception_UnknownFilterException(%s);
|
||||
}
|
||||
$value = call_user_func($filter, $value);%s
|
||||
';
|
||||
|
||||
/**
|
||||
* Generate PHP source for a single filter.
|
||||
*
|
||||
* @param array $filters
|
||||
* @param int $level
|
||||
*
|
||||
* @return string Generated filter PHP source
|
||||
*/
|
||||
private function getFilter(array $filters, $level)
|
||||
{
|
||||
if (empty($filters)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$name = array_shift($filters);
|
||||
$method = $this->getFindMethod($name);
|
||||
$filter = ($method !== 'last') ? var_export($name, true) : '';
|
||||
$callable = $this->getCallable('$filter');
|
||||
$msg = var_export($name, true);
|
||||
|
||||
return sprintf($this->prepare(self::FILTER, $level), $method, $filter, $callable, $msg, $this->getFilter($filters, $level));
|
||||
}
|
||||
|
||||
const LINE = '$buffer .= "\n";';
|
||||
const TEXT = '$buffer .= %s%s;';
|
||||
|
||||
/**
|
||||
* Generate Mustache Template output Buffer call PHP source.
|
||||
*
|
||||
* @param string $text
|
||||
* @param int $level
|
||||
*
|
||||
* @return string Generated output Buffer call PHP source
|
||||
*/
|
||||
private function text($text, $level)
|
||||
{
|
||||
if ($text === "\n") {
|
||||
$this->indentNextLine = true;
|
||||
|
||||
return $this->prepare(self::LINE, $level);
|
||||
} else {
|
||||
return sprintf($this->prepare(self::TEXT, $level), $this->flushIndent(), var_export($text, true));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare PHP source code snippet for output.
|
||||
*
|
||||
* @param string $text
|
||||
* @param int $bonus Additional indent level (default: 0)
|
||||
* @param boolean $prependNewline Prepend a newline to the snippet? (default: true)
|
||||
* @param boolean $appendNewline Append a newline to the snippet? (default: false)
|
||||
*
|
||||
* @return string PHP source code snippet
|
||||
*/
|
||||
private function prepare($text, $bonus = 0, $prependNewline = true, $appendNewline = false)
|
||||
{
|
||||
$text = ($prependNewline ? "\n" : '').trim($text);
|
||||
if ($prependNewline) {
|
||||
$bonus++;
|
||||
}
|
||||
if ($appendNewline) {
|
||||
$text .= "\n";
|
||||
}
|
||||
|
||||
return preg_replace("/\n( {8})?/", "\n".str_repeat(" ", $bonus * 4), $text);
|
||||
}
|
||||
|
||||
const DEFAULT_ESCAPE = 'htmlspecialchars(%s, ENT_COMPAT, %s)';
|
||||
const CUSTOM_ESCAPE = 'call_user_func($this->mustache->getEscape(), %s)';
|
||||
|
||||
/**
|
||||
* Get the current escaper.
|
||||
*
|
||||
* @param string $value (default: '$value')
|
||||
*
|
||||
* @return string Either a custom callback, or an inline call to `htmlspecialchars`
|
||||
*/
|
||||
private function getEscape($value = '$value')
|
||||
{
|
||||
if ($this->customEscape) {
|
||||
return sprintf(self::CUSTOM_ESCAPE, $value);
|
||||
} else {
|
||||
return sprintf(self::DEFAULT_ESCAPE, $value, var_export($this->charset, true));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the appropriate Context `find` method for a given $id.
|
||||
*
|
||||
* The return value will be one of `find`, `findDot` or `last`.
|
||||
*
|
||||
* @see Mustache_Context::find
|
||||
* @see Mustache_Context::findDot
|
||||
* @see Mustache_Context::last
|
||||
*
|
||||
* @param string $id Variable name
|
||||
*
|
||||
* @return string `find` method name
|
||||
*/
|
||||
private function getFindMethod($id)
|
||||
{
|
||||
if ($id === '.') {
|
||||
return 'last';
|
||||
} elseif (strpos($id, '.') === false) {
|
||||
return 'find';
|
||||
} else {
|
||||
return 'findDot';
|
||||
}
|
||||
}
|
||||
|
||||
const IS_CALLABLE = '!is_string(%s) && is_callable(%s)';
|
||||
const STRICT_IS_CALLABLE = 'is_object(%s) && is_callable(%s)';
|
||||
|
||||
private function getCallable($variable = '$value')
|
||||
{
|
||||
$tpl = $this->strictCallables ? self::STRICT_IS_CALLABLE : self::IS_CALLABLE;
|
||||
|
||||
return sprintf($tpl, $variable, $variable);
|
||||
}
|
||||
|
||||
const LINE_INDENT = '$indent . ';
|
||||
|
||||
/**
|
||||
* Get the current $indent prefix to write to the buffer.
|
||||
*
|
||||
* @return string "$indent . " or ""
|
||||
*/
|
||||
private function flushIndent()
|
||||
{
|
||||
if ($this->indentNextLine) {
|
||||
$this->indentNextLine = false;
|
||||
|
||||
return self::LINE_INDENT;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
149
builder/lib/mustache/Context.php
Normal file
149
builder/lib/mustache/Context.php
Normal file
@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Mustache.php.
|
||||
*
|
||||
* (c) 2012 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Mustache Template rendering Context.
|
||||
*/
|
||||
class Mustache_Context
|
||||
{
|
||||
private $stack = array();
|
||||
|
||||
/**
|
||||
* Mustache rendering Context constructor.
|
||||
*
|
||||
* @param mixed $context Default rendering context (default: null)
|
||||
*/
|
||||
public function __construct($context = null)
|
||||
{
|
||||
if ($context !== null) {
|
||||
$this->stack = array($context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a new Context frame onto the stack.
|
||||
*
|
||||
* @param mixed $value Object or array to use for context
|
||||
*/
|
||||
public function push($value)
|
||||
{
|
||||
array_push($this->stack, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pop the last Context frame from the stack.
|
||||
*
|
||||
* @return mixed Last Context frame (object or array)
|
||||
*/
|
||||
public function pop()
|
||||
{
|
||||
return array_pop($this->stack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last Context frame.
|
||||
*
|
||||
* @return mixed Last Context frame (object or array)
|
||||
*/
|
||||
public function last()
|
||||
{
|
||||
return end($this->stack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a variable in the Context stack.
|
||||
*
|
||||
* Starting with the last Context frame (the context of the innermost section), and working back to the top-level
|
||||
* rendering context, look for a variable with the given name:
|
||||
*
|
||||
* * If the Context frame is an associative array which contains the key $id, returns the value of that element.
|
||||
* * If the Context frame is an object, this will check first for a public method, then a public property named
|
||||
* $id. Failing both of these, it will try `__isset` and `__get` magic methods.
|
||||
* * If a value named $id is not found in any Context frame, returns an empty string.
|
||||
*
|
||||
* @param string $id Variable name
|
||||
*
|
||||
* @return mixed Variable value, or '' if not found
|
||||
*/
|
||||
public function find($id)
|
||||
{
|
||||
return $this->findVariableInStack($id, $this->stack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a 'dot notation' variable in the Context stack.
|
||||
*
|
||||
* Note that dot notation traversal bubbles through scope differently than the regular find method. After finding
|
||||
* the initial chunk of the dotted name, each subsequent chunk is searched for only within the value of the previous
|
||||
* result. For example, given the following context stack:
|
||||
*
|
||||
* $data = array(
|
||||
* 'name' => 'Fred',
|
||||
* 'child' => array(
|
||||
* 'name' => 'Bob'
|
||||
* ),
|
||||
* );
|
||||
*
|
||||
* ... and the Mustache following template:
|
||||
*
|
||||
* {{ child.name }}
|
||||
*
|
||||
* ... the `name` value is only searched for within the `child` value of the global Context, not within parent
|
||||
* Context frames.
|
||||
*
|
||||
* @param string $id Dotted variable selector
|
||||
*
|
||||
* @return mixed Variable value, or '' if not found
|
||||
*/
|
||||
public function findDot($id)
|
||||
{
|
||||
$chunks = explode('.', $id);
|
||||
$first = array_shift($chunks);
|
||||
$value = $this->findVariableInStack($first, $this->stack);
|
||||
|
||||
foreach ($chunks as $chunk) {
|
||||
if ($value === '') {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$value = $this->findVariableInStack($chunk, array($value));
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to find a variable in the Context stack.
|
||||
*
|
||||
* @see Mustache_Context::find
|
||||
*
|
||||
* @param string $id Variable name
|
||||
* @param array $stack Context stack
|
||||
*
|
||||
* @return mixed Variable value, or '' if not found
|
||||
*/
|
||||
private function findVariableInStack($id, array $stack)
|
||||
{
|
||||
for ($i = count($stack) - 1; $i >= 0; $i--) {
|
||||
if (is_object($stack[$i]) && !$stack[$i] instanceof Closure) {
|
||||
if (method_exists($stack[$i], $id)) {
|
||||
return $stack[$i]->$id();
|
||||
} elseif (isset($stack[$i]->$id)) {
|
||||
return $stack[$i]->$id;
|
||||
}
|
||||
} elseif (is_array($stack[$i]) && array_key_exists($id, $stack[$i])) {
|
||||
return $stack[$i][$id];
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
729
builder/lib/mustache/Engine.php
Normal file
729
builder/lib/mustache/Engine.php
Normal file
@ -0,0 +1,729 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Mustache.php.
|
||||
*
|
||||
* (c) 2012 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A Mustache implementation in PHP.
|
||||
*
|
||||
* {@link http://defunkt.github.com/mustache}
|
||||
*
|
||||
* Mustache is a framework-agnostic logic-less templating language. It enforces separation of view
|
||||
* logic from template files. In fact, it is not even possible to embed logic in the template.
|
||||
*
|
||||
* This is very, very rad.
|
||||
*
|
||||
* @author Justin Hileman {@link http://justinhileman.com}
|
||||
*/
|
||||
class Mustache_Engine
|
||||
{
|
||||
const VERSION = '2.3.1';
|
||||
const SPEC_VERSION = '1.1.2';
|
||||
|
||||
const PRAGMA_FILTERS = 'FILTERS';
|
||||
|
||||
// Template cache
|
||||
private $templates = array();
|
||||
|
||||
// Environment
|
||||
private $templateClassPrefix = '__Mustache_';
|
||||
private $cache = null;
|
||||
private $cacheFileMode = null;
|
||||
private $loader;
|
||||
private $partialsLoader;
|
||||
private $helpers;
|
||||
private $escape;
|
||||
private $charset = 'UTF-8';
|
||||
private $logger;
|
||||
private $strictCallables = false;
|
||||
|
||||
/**
|
||||
* Mustache class constructor.
|
||||
*
|
||||
* Passing an $options array allows overriding certain Mustache options during instantiation:
|
||||
*
|
||||
* $options = array(
|
||||
* // The class prefix for compiled templates. Defaults to '__Mustache_'.
|
||||
* 'template_class_prefix' => '__MyTemplates_',
|
||||
*
|
||||
* // A cache directory for compiled templates. Mustache will not cache templates unless this is set
|
||||
* 'cache' => dirname(__FILE__).'/tmp/cache/mustache',
|
||||
*
|
||||
* // Override default permissions for cache files. Defaults to using the system-defined umask. It is
|
||||
* // *strongly* recommended that you configure your umask properly rather than overriding permissions here.
|
||||
* 'cache_file_mode' => 0666,
|
||||
*
|
||||
* // A Mustache template loader instance. Uses a StringLoader if not specified.
|
||||
* 'loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views'),
|
||||
*
|
||||
* // A Mustache loader instance for partials.
|
||||
* 'partials_loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views/partials'),
|
||||
*
|
||||
* // An array of Mustache partials. Useful for quick-and-dirty string template loading, but not as
|
||||
* // efficient or lazy as a Filesystem (or database) loader.
|
||||
* 'partials' => array('foo' => file_get_contents(dirname(__FILE__).'/views/partials/foo.mustache')),
|
||||
*
|
||||
* // An array of 'helpers'. Helpers can be global variables or objects, closures (e.g. for higher order
|
||||
* // sections), or any other valid Mustache context value. They will be prepended to the context stack,
|
||||
* // so they will be available in any template loaded by this Mustache instance.
|
||||
* 'helpers' => array('i18n' => function($text) {
|
||||
* // do something translatey here...
|
||||
* }),
|
||||
*
|
||||
* // An 'escape' callback, responsible for escaping double-mustache variables.
|
||||
* 'escape' => function($value) {
|
||||
* return htmlspecialchars($buffer, ENT_COMPAT, 'UTF-8');
|
||||
* },
|
||||
*
|
||||
* // Character set for `htmlspecialchars`. Defaults to 'UTF-8'. Use 'UTF-8'.
|
||||
* 'charset' => 'ISO-8859-1',
|
||||
*
|
||||
* // A Mustache Logger instance. No logging will occur unless this is set. Using a PSR-3 compatible
|
||||
* // logging library -- such as Monolog -- is highly recommended. A simple stream logger implementation is
|
||||
* // available as well:
|
||||
* 'logger' => new Mustache_Logger_StreamLogger('php://stderr'),
|
||||
*
|
||||
* // Only treat Closure instances and invokable classes as callable. If true, values like
|
||||
* // `array('ClassName', 'methodName')` and `array($classInstance, 'methodName')`, which are traditionally
|
||||
* // "callable" in PHP, are not called to resolve variables for interpolation or section contexts. This
|
||||
* // helps protect against arbitrary code execution when user input is passed directly into the template.
|
||||
* // This currently defaults to false, but will default to true in v3.0.
|
||||
* 'strict_callables' => true,
|
||||
* );
|
||||
*
|
||||
* @throws Mustache_Exception_InvalidArgumentException If `escape` option is not callable.
|
||||
*
|
||||
* @param array $options (default: array())
|
||||
*/
|
||||
public function __construct(array $options = array())
|
||||
{
|
||||
if (isset($options['template_class_prefix'])) {
|
||||
$this->templateClassPrefix = $options['template_class_prefix'];
|
||||
}
|
||||
|
||||
if (isset($options['cache'])) {
|
||||
$this->cache = $options['cache'];
|
||||
}
|
||||
|
||||
if (isset($options['cache_file_mode'])) {
|
||||
$this->cacheFileMode = $options['cache_file_mode'];
|
||||
}
|
||||
|
||||
if (isset($options['loader'])) {
|
||||
$this->setLoader($options['loader']);
|
||||
}
|
||||
|
||||
if (isset($options['partials_loader'])) {
|
||||
$this->setPartialsLoader($options['partials_loader']);
|
||||
}
|
||||
|
||||
if (isset($options['partials'])) {
|
||||
$this->setPartials($options['partials']);
|
||||
}
|
||||
|
||||
if (isset($options['helpers'])) {
|
||||
$this->setHelpers($options['helpers']);
|
||||
}
|
||||
|
||||
if (isset($options['escape'])) {
|
||||
if (!is_callable($options['escape'])) {
|
||||
throw new Mustache_Exception_InvalidArgumentException('Mustache Constructor "escape" option must be callable');
|
||||
}
|
||||
|
||||
$this->escape = $options['escape'];
|
||||
}
|
||||
|
||||
if (isset($options['charset'])) {
|
||||
$this->charset = $options['charset'];
|
||||
}
|
||||
|
||||
if (isset($options['logger'])) {
|
||||
$this->setLogger($options['logger']);
|
||||
}
|
||||
|
||||
if (isset($options['strict_callables'])) {
|
||||
$this->strictCallables = $options['strict_callables'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut 'render' invocation.
|
||||
*
|
||||
* Equivalent to calling `$mustache->loadTemplate($template)->render($context);`
|
||||
*
|
||||
* @see Mustache_Engine::loadTemplate
|
||||
* @see Mustache_Template::render
|
||||
*
|
||||
* @param string $template
|
||||
* @param mixed $context (default: array())
|
||||
*
|
||||
* @return string Rendered template
|
||||
*/
|
||||
public function render($template, $context = array())
|
||||
{
|
||||
return $this->loadTemplate($template)->render($context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current Mustache escape callback.
|
||||
*
|
||||
* @return mixed Callable or null
|
||||
*/
|
||||
public function getEscape()
|
||||
{
|
||||
return $this->escape;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current Mustache character set.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCharset()
|
||||
{
|
||||
return $this->charset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Mustache template Loader instance.
|
||||
*
|
||||
* @param Mustache_Loader $loader
|
||||
*/
|
||||
public function setLoader(Mustache_Loader $loader)
|
||||
{
|
||||
$this->loader = $loader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current Mustache template Loader instance.
|
||||
*
|
||||
* If no Loader instance has been explicitly specified, this method will instantiate and return
|
||||
* a StringLoader instance.
|
||||
*
|
||||
* @return Mustache_Loader
|
||||
*/
|
||||
public function getLoader()
|
||||
{
|
||||
if (!isset($this->loader)) {
|
||||
$this->loader = new Mustache_Loader_StringLoader;
|
||||
}
|
||||
|
||||
return $this->loader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Mustache partials Loader instance.
|
||||
*
|
||||
* @param Mustache_Loader $partialsLoader
|
||||
*/
|
||||
public function setPartialsLoader(Mustache_Loader $partialsLoader)
|
||||
{
|
||||
$this->partialsLoader = $partialsLoader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current Mustache partials Loader instance.
|
||||
*
|
||||
* If no Loader instance has been explicitly specified, this method will instantiate and return
|
||||
* an ArrayLoader instance.
|
||||
*
|
||||
* @return Mustache_Loader
|
||||
*/
|
||||
public function getPartialsLoader()
|
||||
{
|
||||
if (!isset($this->partialsLoader)) {
|
||||
$this->partialsLoader = new Mustache_Loader_ArrayLoader;
|
||||
}
|
||||
|
||||
return $this->partialsLoader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set partials for the current partials Loader instance.
|
||||
*
|
||||
* @throws Mustache_Exception_RuntimeException If the current Loader instance is immutable
|
||||
*
|
||||
* @param array $partials (default: array())
|
||||
*/
|
||||
public function setPartials(array $partials = array())
|
||||
{
|
||||
if (!isset($this->partialsLoader)) {
|
||||
$this->partialsLoader = new Mustache_Loader_ArrayLoader;
|
||||
}
|
||||
|
||||
if (!$this->partialsLoader instanceof Mustache_Loader_MutableLoader) {
|
||||
throw new Mustache_Exception_RuntimeException('Unable to set partials on an immutable Mustache Loader instance');
|
||||
}
|
||||
|
||||
$this->partialsLoader->setTemplates($partials);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an array of Mustache helpers.
|
||||
*
|
||||
* An array of 'helpers'. Helpers can be global variables or objects, closures (e.g. for higher order sections), or
|
||||
* any other valid Mustache context value. They will be prepended to the context stack, so they will be available in
|
||||
* any template loaded by this Mustache instance.
|
||||
*
|
||||
* @throws Mustache_Exception_InvalidArgumentException if $helpers is not an array or Traversable
|
||||
*
|
||||
* @param array|Traversable $helpers
|
||||
*/
|
||||
public function setHelpers($helpers)
|
||||
{
|
||||
if (!is_array($helpers) && !$helpers instanceof Traversable) {
|
||||
throw new Mustache_Exception_InvalidArgumentException('setHelpers expects an array of helpers');
|
||||
}
|
||||
|
||||
$this->getHelpers()->clear();
|
||||
|
||||
foreach ($helpers as $name => $helper) {
|
||||
$this->addHelper($name, $helper);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current set of Mustache helpers.
|
||||
*
|
||||
* @see Mustache_Engine::setHelpers
|
||||
*
|
||||
* @return Mustache_HelperCollection
|
||||
*/
|
||||
public function getHelpers()
|
||||
{
|
||||
if (!isset($this->helpers)) {
|
||||
$this->helpers = new Mustache_HelperCollection;
|
||||
}
|
||||
|
||||
return $this->helpers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new Mustache helper.
|
||||
*
|
||||
* @see Mustache_Engine::setHelpers
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed $helper
|
||||
*/
|
||||
public function addHelper($name, $helper)
|
||||
{
|
||||
$this->getHelpers()->add($name, $helper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a Mustache helper by name.
|
||||
*
|
||||
* @see Mustache_Engine::setHelpers
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return mixed Helper
|
||||
*/
|
||||
public function getHelper($name)
|
||||
{
|
||||
return $this->getHelpers()->get($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether this Mustache instance has a helper.
|
||||
*
|
||||
* @see Mustache_Engine::setHelpers
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return boolean True if the helper is present
|
||||
*/
|
||||
public function hasHelper($name)
|
||||
{
|
||||
return $this->getHelpers()->has($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a helper by name.
|
||||
*
|
||||
* @see Mustache_Engine::setHelpers
|
||||
*
|
||||
* @param string $name
|
||||
*/
|
||||
public function removeHelper($name)
|
||||
{
|
||||
$this->getHelpers()->remove($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Mustache Logger instance.
|
||||
*
|
||||
* @throws Mustache_Exception_InvalidArgumentException If logger is not an instance of Mustache_Logger or Psr\Log\LoggerInterface.
|
||||
*
|
||||
* @param Mustache_Logger|Psr\Log\LoggerInterface $logger
|
||||
*/
|
||||
public function setLogger($logger = null)
|
||||
{
|
||||
if ($logger !== null && !($logger instanceof Mustache_Logger || is_a($logger, 'Psr\\Log\\LoggerInterface'))) {
|
||||
throw new Mustache_Exception_InvalidArgumentException('Expected an instance of Mustache_Logger or Psr\\Log\\LoggerInterface.');
|
||||
}
|
||||
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current Mustache Logger instance.
|
||||
*
|
||||
* @return Mustache_Logger|Psr\Log\LoggerInterface
|
||||
*/
|
||||
public function getLogger()
|
||||
{
|
||||
return $this->logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Mustache Tokenizer instance.
|
||||
*
|
||||
* @param Mustache_Tokenizer $tokenizer
|
||||
*/
|
||||
public function setTokenizer(Mustache_Tokenizer $tokenizer)
|
||||
{
|
||||
$this->tokenizer = $tokenizer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current Mustache Tokenizer instance.
|
||||
*
|
||||
* If no Tokenizer instance has been explicitly specified, this method will instantiate and return a new one.
|
||||
*
|
||||
* @return Mustache_Tokenizer
|
||||
*/
|
||||
public function getTokenizer()
|
||||
{
|
||||
if (!isset($this->tokenizer)) {
|
||||
$this->tokenizer = new Mustache_Tokenizer;
|
||||
}
|
||||
|
||||
return $this->tokenizer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Mustache Parser instance.
|
||||
*
|
||||
* @param Mustache_Parser $parser
|
||||
*/
|
||||
public function setParser(Mustache_Parser $parser)
|
||||
{
|
||||
$this->parser = $parser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current Mustache Parser instance.
|
||||
*
|
||||
* If no Parser instance has been explicitly specified, this method will instantiate and return a new one.
|
||||
*
|
||||
* @return Mustache_Parser
|
||||
*/
|
||||
public function getParser()
|
||||
{
|
||||
if (!isset($this->parser)) {
|
||||
$this->parser = new Mustache_Parser;
|
||||
}
|
||||
|
||||
return $this->parser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Mustache Compiler instance.
|
||||
*
|
||||
* @param Mustache_Compiler $compiler
|
||||
*/
|
||||
public function setCompiler(Mustache_Compiler $compiler)
|
||||
{
|
||||
$this->compiler = $compiler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current Mustache Compiler instance.
|
||||
*
|
||||
* If no Compiler instance has been explicitly specified, this method will instantiate and return a new one.
|
||||
*
|
||||
* @return Mustache_Compiler
|
||||
*/
|
||||
public function getCompiler()
|
||||
{
|
||||
if (!isset($this->compiler)) {
|
||||
$this->compiler = new Mustache_Compiler;
|
||||
}
|
||||
|
||||
return $this->compiler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to generate a Mustache template class.
|
||||
*
|
||||
* @param string $source
|
||||
*
|
||||
* @return string Mustache Template class name
|
||||
*/
|
||||
public function getTemplateClassName($source)
|
||||
{
|
||||
return $this->templateClassPrefix . md5(sprintf(
|
||||
'version:%s,escape:%s,charset:%s,strict_callables:%s,source:%s',
|
||||
self::VERSION,
|
||||
isset($this->escape) ? 'custom' : 'default',
|
||||
$this->charset,
|
||||
$this->strictCallables ? 'true' : 'false',
|
||||
$source
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a Mustache Template by name.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return Mustache_Template
|
||||
*/
|
||||
public function loadTemplate($name)
|
||||
{
|
||||
return $this->loadSource($this->getLoader()->load($name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a Mustache partial Template by name.
|
||||
*
|
||||
* This is a helper method used internally by Template instances for loading partial templates. You can most likely
|
||||
* ignore it completely.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return Mustache_Template
|
||||
*/
|
||||
public function loadPartial($name)
|
||||
{
|
||||
try {
|
||||
if (isset($this->partialsLoader)) {
|
||||
$loader = $this->partialsLoader;
|
||||
} elseif (isset($this->loader) && !$this->loader instanceof Mustache_Loader_StringLoader) {
|
||||
$loader = $this->loader;
|
||||
} else {
|
||||
throw new Mustache_Exception_UnknownTemplateException($name);
|
||||
}
|
||||
|
||||
return $this->loadSource($loader->load($name));
|
||||
} catch (Mustache_Exception_UnknownTemplateException $e) {
|
||||
// If the named partial cannot be found, log then return null.
|
||||
$this->log(
|
||||
Mustache_Logger::WARNING,
|
||||
'Partial not found: "{name}"',
|
||||
array('name' => $e->getTemplateName())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a Mustache lambda Template by source.
|
||||
*
|
||||
* This is a helper method used by Template instances to generate subtemplates for Lambda sections. You can most
|
||||
* likely ignore it completely.
|
||||
*
|
||||
* @param string $source
|
||||
* @param string $delims (default: null)
|
||||
*
|
||||
* @return Mustache_Template
|
||||
*/
|
||||
public function loadLambda($source, $delims = null)
|
||||
{
|
||||
if ($delims !== null) {
|
||||
$source = $delims . "\n" . $source;
|
||||
}
|
||||
|
||||
return $this->loadSource($source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate and return a Mustache Template instance by source.
|
||||
*
|
||||
* @see Mustache_Engine::loadTemplate
|
||||
* @see Mustache_Engine::loadPartial
|
||||
* @see Mustache_Engine::loadLambda
|
||||
*
|
||||
* @param string $source
|
||||
*
|
||||
* @return Mustache_Template
|
||||
*/
|
||||
private function loadSource($source)
|
||||
{
|
||||
$className = $this->getTemplateClassName($source);
|
||||
|
||||
if (!isset($this->templates[$className])) {
|
||||
if (!class_exists($className, false)) {
|
||||
if ($fileName = $this->getCacheFilename($source)) {
|
||||
if (!is_file($fileName)) {
|
||||
$this->log(
|
||||
Mustache_Logger::DEBUG,
|
||||
'Writing "{className}" class to template cache: "{fileName}"',
|
||||
array('className' => $className, 'fileName' => $fileName)
|
||||
);
|
||||
|
||||
$this->writeCacheFile($fileName, $this->compile($source));
|
||||
}
|
||||
|
||||
require_once $fileName;
|
||||
} else {
|
||||
$this->log(
|
||||
Mustache_Logger::WARNING,
|
||||
'Template cache disabled, evaluating "{className}" class at runtime',
|
||||
array('className' => $className)
|
||||
);
|
||||
|
||||
eval('?>'.$this->compile($source));
|
||||
}
|
||||
}
|
||||
|
||||
$this->log(
|
||||
Mustache_Logger::DEBUG,
|
||||
'Instantiating template: "{className}"',
|
||||
array('className' => $className)
|
||||
);
|
||||
|
||||
$this->templates[$className] = new $className($this);
|
||||
}
|
||||
|
||||
return $this->templates[$className];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to tokenize a Mustache template.
|
||||
*
|
||||
* @see Mustache_Tokenizer::scan
|
||||
*
|
||||
* @param string $source
|
||||
*
|
||||
* @return array Tokens
|
||||
*/
|
||||
private function tokenize($source)
|
||||
{
|
||||
return $this->getTokenizer()->scan($source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to parse a Mustache template.
|
||||
*
|
||||
* @see Mustache_Parser::parse
|
||||
*
|
||||
* @param string $source
|
||||
*
|
||||
* @return array Token tree
|
||||
*/
|
||||
private function parse($source)
|
||||
{
|
||||
return $this->getParser()->parse($this->tokenize($source));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to compile a Mustache template.
|
||||
*
|
||||
* @see Mustache_Compiler::compile
|
||||
*
|
||||
* @param string $source
|
||||
*
|
||||
* @return string generated Mustache template class code
|
||||
*/
|
||||
private function compile($source)
|
||||
{
|
||||
$tree = $this->parse($source);
|
||||
$name = $this->getTemplateClassName($source);
|
||||
|
||||
$this->log(
|
||||
Mustache_Logger::INFO,
|
||||
'Compiling template to "{className}" class',
|
||||
array('className' => $name)
|
||||
);
|
||||
|
||||
return $this->getCompiler()->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to generate a Mustache Template class cache filename.
|
||||
*
|
||||
* @param string $source
|
||||
*
|
||||
* @return string Mustache Template class cache filename
|
||||
*/
|
||||
private function getCacheFilename($source)
|
||||
{
|
||||
if ($this->cache) {
|
||||
return sprintf('%s/%s.php', $this->cache, $this->getTemplateClassName($source));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to dump a generated Mustache Template subclass to the file cache.
|
||||
*
|
||||
* @throws Mustache_Exception_RuntimeException if unable to create the cache directory or write to $fileName.
|
||||
*
|
||||
* @param string $fileName
|
||||
* @param string $source
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function writeCacheFile($fileName, $source)
|
||||
{
|
||||
$dirName = dirname($fileName);
|
||||
if (!is_dir($dirName)) {
|
||||
$this->log(
|
||||
Mustache_Logger::INFO,
|
||||
'Creating Mustache template cache directory: "{dirName}"',
|
||||
array('dirName' => $dirName)
|
||||
);
|
||||
|
||||
@mkdir($dirName, 0777, true);
|
||||
if (!is_dir($dirName)) {
|
||||
throw new Mustache_Exception_RuntimeException(sprintf('Failed to create cache directory "%s".', $dirName));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$this->log(
|
||||
Mustache_Logger::DEBUG,
|
||||
'Caching compiled template to "{fileName}"',
|
||||
array('fileName' => $fileName)
|
||||
);
|
||||
|
||||
$tempFile = tempnam($dirName, basename($fileName));
|
||||
if (false !== @file_put_contents($tempFile, $source)) {
|
||||
if (@rename($tempFile, $fileName)) {
|
||||
$mode = isset($this->cacheFileMode) ? $this->cacheFileMode : (0666 & ~umask());
|
||||
@chmod($fileName, $mode);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->log(
|
||||
Mustache_Logger::ERROR,
|
||||
'Unable to rename Mustache temp cache file: "{tempName}" -> "{fileName}"',
|
||||
array('tempName' => $tempFile, 'fileName' => $fileName)
|
||||
);
|
||||
}
|
||||
|
||||
throw new Mustache_Exception_RuntimeException(sprintf('Failed to write cache file "%s".', $fileName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a log record if logging is enabled.
|
||||
*
|
||||
* @param integer $level The logging level
|
||||
* @param string $message The log message
|
||||
* @param array $context The log context
|
||||
*/
|
||||
private function log($level, $message, array $context = array())
|
||||
{
|
||||
if (isset($this->logger)) {
|
||||
$this->logger->log($level, $message, $context);
|
||||
}
|
||||
}
|
||||
}
|
18
builder/lib/mustache/Exception.php
Normal file
18
builder/lib/mustache/Exception.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Mustache.php.
|
||||
*
|
||||
* (c) 2013 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A Mustache Exception interface.
|
||||
*/
|
||||
interface Mustache_Exception
|
||||
{
|
||||
// This space intentionally left blank.
|
||||
}
|
18
builder/lib/mustache/Exception/InvalidArgumentException.php
Normal file
18
builder/lib/mustache/Exception/InvalidArgumentException.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Mustache.php.
|
||||
*
|
||||
* (c) 2013 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Invalid argument exception.
|
||||
*/
|
||||
class Mustache_Exception_InvalidArgumentException extends InvalidArgumentException implements Mustache_Exception
|
||||
{
|
||||
// This space intentionally left blank.
|
||||
}
|
18
builder/lib/mustache/Exception/LogicException.php
Normal file
18
builder/lib/mustache/Exception/LogicException.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Mustache.php.
|
||||
*
|
||||
* (c) 2013 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Logic exception.
|
||||
*/
|
||||
class Mustache_Exception_LogicException extends LogicException implements Mustache_Exception
|
||||
{
|
||||
// This space intentionally left blank.
|
||||
}
|
18
builder/lib/mustache/Exception/RuntimeException.php
Normal file
18
builder/lib/mustache/Exception/RuntimeException.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Mustache.php.
|
||||
*
|
||||
* (c) 2013 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Runtime exception.
|
||||
*/
|
||||
class Mustache_Exception_RuntimeException extends RuntimeException implements Mustache_Exception
|
||||
{
|
||||
// This space intentionally left blank.
|
||||
}
|
29
builder/lib/mustache/Exception/SyntaxException.php
Normal file
29
builder/lib/mustache/Exception/SyntaxException.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Mustache.php.
|
||||
*
|
||||
* (c) 2013 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Mustache syntax exception.
|
||||
*/
|
||||
class Mustache_Exception_SyntaxException extends LogicException implements Mustache_Exception
|
||||
{
|
||||
protected $token;
|
||||
|
||||
public function __construct($msg, array $token)
|
||||
{
|
||||
$this->token = $token;
|
||||
parent::__construct($msg);
|
||||
}
|
||||
|
||||
public function getToken()
|
||||
{
|
||||
return $this->token;
|
||||
}
|
||||
}
|
29
builder/lib/mustache/Exception/UnknownFilterException.php
Normal file
29
builder/lib/mustache/Exception/UnknownFilterException.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Mustache.php.
|
||||
*
|
||||
* (c) 2013 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Unknown filter exception.
|
||||
*/
|
||||
class Mustache_Exception_UnknownFilterException extends UnexpectedValueException implements Mustache_Exception
|
||||
{
|
||||
protected $filterName;
|
||||
|
||||
public function __construct($filterName)
|
||||
{
|
||||
$this->filterName = $filterName;
|
||||
parent::__construct(sprintf('Unknown filter: %s', $filterName));
|
||||
}
|
||||
|
||||
public function getFilterName()
|
||||
{
|
||||
return $this->filterName;
|
||||
}
|
||||
}
|
29
builder/lib/mustache/Exception/UnknownHelperException.php
Normal file
29
builder/lib/mustache/Exception/UnknownHelperException.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Mustache.php.
|
||||
*
|
||||
* (c) 2013 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Unknown helper exception.
|
||||
*/
|
||||
class Mustache_Exception_UnknownHelperException extends InvalidArgumentException implements Mustache_Exception
|
||||
{
|
||||
protected $helperName;
|
||||
|
||||
public function __construct($helperName)
|
||||
{
|
||||
$this->helperName = $helperName;
|
||||
parent::__construct(sprintf('Unknown helper: %s', $helperName));
|
||||
}
|
||||
|
||||
public function getHelperName()
|
||||
{
|
||||
return $this->helperName;
|
||||
}
|
||||
}
|
29
builder/lib/mustache/Exception/UnknownTemplateException.php
Normal file
29
builder/lib/mustache/Exception/UnknownTemplateException.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Mustache.php.
|
||||
*
|
||||
* (c) 2013 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Unknown template exception.
|
||||
*/
|
||||
class Mustache_Exception_UnknownTemplateException extends InvalidArgumentException implements Mustache_Exception
|
||||
{
|
||||
protected $templateName;
|
||||
|
||||
public function __construct($templateName)
|
||||
{
|
||||
$this->templateName = $templateName;
|
||||
parent::__construct(sprintf('Unknown template: %s', $templateName));
|
||||
}
|
||||
|
||||
public function getTemplateName()
|
||||
{
|
||||
return $this->templateName;
|
||||
}
|
||||
}
|
170
builder/lib/mustache/HelperCollection.php
Normal file
170
builder/lib/mustache/HelperCollection.php
Normal file
@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Mustache.php.
|
||||
*
|
||||
* (c) 2012 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A collection of helpers for a Mustache instance.
|
||||
*/
|
||||
class Mustache_HelperCollection
|
||||
{
|
||||
private $helpers = array();
|
||||
|
||||
/**
|
||||
* Helper Collection constructor.
|
||||
*
|
||||
* Optionally accepts an array (or Traversable) of `$name => $helper` pairs.
|
||||
*
|
||||
* @throws Mustache_Exception_InvalidArgumentException if the $helpers argument isn't an array or Traversable
|
||||
*
|
||||
* @param array|Traversable $helpers (default: null)
|
||||
*/
|
||||
public function __construct($helpers = null)
|
||||
{
|
||||
if ($helpers !== null) {
|
||||
if (!is_array($helpers) && !$helpers instanceof Traversable) {
|
||||
throw new Mustache_Exception_InvalidArgumentException('HelperCollection constructor expects an array of helpers');
|
||||
}
|
||||
|
||||
foreach ($helpers as $name => $helper) {
|
||||
$this->add($name, $helper);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic mutator.
|
||||
*
|
||||
* @see Mustache_HelperCollection::add
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed $helper
|
||||
*/
|
||||
public function __set($name, $helper)
|
||||
{
|
||||
$this->add($name, $helper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a helper to this collection.
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed $helper
|
||||
*/
|
||||
public function add($name, $helper)
|
||||
{
|
||||
$this->helpers[$name] = $helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic accessor.
|
||||
*
|
||||
* @see Mustache_HelperCollection::get
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return mixed Helper
|
||||
*/
|
||||
public function __get($name)
|
||||
{
|
||||
return $this->get($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a helper by name.
|
||||
*
|
||||
* @throws Mustache_Exception_UnknownHelperException If helper does not exist.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return mixed Helper
|
||||
*/
|
||||
public function get($name)
|
||||
{
|
||||
if (!$this->has($name)) {
|
||||
throw new Mustache_Exception_UnknownHelperException($name);
|
||||
}
|
||||
|
||||
return $this->helpers[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic isset().
|
||||
*
|
||||
* @see Mustache_HelperCollection::has
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return boolean True if helper is present
|
||||
*/
|
||||
public function __isset($name)
|
||||
{
|
||||
return $this->has($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a given helper is present in the collection.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return boolean True if helper is present
|
||||
*/
|
||||
public function has($name)
|
||||
{
|
||||
return array_key_exists($name, $this->helpers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic unset().
|
||||
*
|
||||
* @see Mustache_HelperCollection::remove
|
||||
*
|
||||
* @param string $name
|
||||
*/
|
||||
public function __unset($name)
|
||||
{
|
||||
$this->remove($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a given helper is present in the collection.
|
||||
*
|
||||
* @throws Mustache_Exception_UnknownHelperException if the requested helper is not present.
|
||||
*
|
||||
* @param string $name
|
||||
*/
|
||||
public function remove($name)
|
||||
{
|
||||
if (!$this->has($name)) {
|
||||
throw new Mustache_Exception_UnknownHelperException($name);
|
||||
}
|
||||
|
||||
unset($this->helpers[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the helper collection.
|
||||
*
|
||||
* Removes all helpers from this collection
|
||||
*/
|
||||
public function clear()
|
||||
{
|
||||
$this->helpers = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the helper collection is empty.
|
||||
*
|
||||
* @return boolean True if the collection is empty
|
||||
*/
|
||||
public function isEmpty()
|
||||
{
|
||||
return empty($this->helpers);
|
||||
}
|
||||
}
|
20
builder/lib/mustache/LICENSE
Normal file
20
builder/lib/mustache/LICENSE
Normal file
@ -0,0 +1,20 @@
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2010 Justin Hileman
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
||||
OR OTHER DEALINGS IN THE SOFTWARE.
|
49
builder/lib/mustache/LambdaHelper.php
Normal file
49
builder/lib/mustache/LambdaHelper.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Mustache.php.
|
||||
*
|
||||
* (c) 2012 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Mustache Lambda Helper.
|
||||
*
|
||||
* Passed as the second argument to section lambdas (higher order sections),
|
||||
* giving them access to a `render` method for rendering a string with the
|
||||
* current context.
|
||||
*/
|
||||
class Mustache_LambdaHelper
|
||||
{
|
||||
private $mustache;
|
||||
private $context;
|
||||
|
||||
/**
|
||||
* Mustache Lambda Helper constructor.
|
||||
*
|
||||
* @param Mustache_Engine $mustache Mustache engine instance.
|
||||
* @param Mustache_Context $context Rendering context.
|
||||
*/
|
||||
public function __construct(Mustache_Engine $mustache, Mustache_Context $context)
|
||||
{
|
||||
$this->mustache = $mustache;
|
||||
$this->context = $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a string as a Mustache template with the current rendering context.
|
||||
*
|
||||
* @param string $string
|
||||
*
|
||||
* @return Rendered template.
|
||||
*/
|
||||
public function render($string)
|
||||
{
|
||||
return $this->mustache
|
||||
->loadLambda((string) $string)
|
||||
->renderInternal($this->context);
|
||||
}
|
||||
}
|
28
builder/lib/mustache/Loader.php
Normal file
28
builder/lib/mustache/Loader.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Mustache.php.
|
||||
*
|
||||
* (c) 2012 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Mustache Template Loader interface.
|
||||
*/
|
||||
interface Mustache_Loader
|
||||
{
|
||||
|
||||
/**
|
||||
* Load a Template by name.
|
||||
*
|
||||
* @throws Mustache_Exception_UnknownTemplateException If a template file is not found.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return string Mustache Template source
|
||||
*/
|
||||
public function load($name);
|
||||
}
|
78
builder/lib/mustache/Loader/ArrayLoader.php
Normal file
78
builder/lib/mustache/Loader/ArrayLoader.php
Normal file
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Mustache.php.
|
||||
*
|
||||
* (c) 2012 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Mustache Template array Loader implementation.
|
||||
*
|
||||
* An ArrayLoader instance loads Mustache Template source by name from an initial array:
|
||||
*
|
||||
* $loader = new ArrayLoader(
|
||||
* 'foo' => '{{ bar }}',
|
||||
* 'baz' => 'Hey {{ qux }}!'
|
||||
* );
|
||||
*
|
||||
* $tpl = $loader->load('foo'); // '{{ bar }}'
|
||||
*
|
||||
* The ArrayLoader is used internally as a partials loader by Mustache_Engine instance when an array of partials
|
||||
* is set. It can also be used as a quick-and-dirty Template loader.
|
||||
*/
|
||||
class Mustache_Loader_ArrayLoader implements Mustache_Loader, Mustache_Loader_MutableLoader
|
||||
{
|
||||
|
||||
/**
|
||||
* ArrayLoader constructor.
|
||||
*
|
||||
* @param array $templates Associative array of Template source (default: array())
|
||||
*/
|
||||
public function __construct(array $templates = array())
|
||||
{
|
||||
$this->templates = $templates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a Template.
|
||||
*
|
||||
* @throws Mustache_Exception_UnknownTemplateException If a template file is not found.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return string Mustache Template source
|
||||
*/
|
||||
public function load($name)
|
||||
{
|
||||
if (!isset($this->templates[$name])) {
|
||||
throw new Mustache_Exception_UnknownTemplateException($name);
|
||||
}
|
||||
|
||||
return $this->templates[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an associative array of Template sources for this loader.
|
||||
*
|
||||
* @param array $templates
|
||||
*/
|
||||
public function setTemplates(array $templates)
|
||||
{
|
||||
$this->templates = $templates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a Template source by name.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $template Mustache Template source
|
||||
*/
|
||||
public function setTemplate($name, $template)
|
||||
{
|
||||
$this->templates[$name] = $template;
|
||||
}
|
||||
}
|
69
builder/lib/mustache/Loader/CascadingLoader.php
Normal file
69
builder/lib/mustache/Loader/CascadingLoader.php
Normal file
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Mustache.php.
|
||||
*
|
||||
* (c) 2013 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A Mustache Template cascading loader implementation, which delegates to other
|
||||
* Loader instances.
|
||||
*/
|
||||
class Mustache_Loader_CascadingLoader implements Mustache_Loader
|
||||
{
|
||||
private $loaders;
|
||||
|
||||
/**
|
||||
* Construct a CascadingLoader with an array of loaders:
|
||||
*
|
||||
* $loader = new Mustache_Loader_CascadingLoader(array(
|
||||
* new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__),
|
||||
* new Mustache_Loader_FilesystemLoader(__DIR__.'/templates')
|
||||
* ));
|
||||
*
|
||||
* @param array $loaders An array of Mustache Loader instances
|
||||
*/
|
||||
public function __construct(array $loaders = array())
|
||||
{
|
||||
$this->loaders = array();
|
||||
foreach ($loaders as $loader) {
|
||||
$this->addLoader($loader);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a Loader instance.
|
||||
*
|
||||
* @param Mustache_Loader $loader A Mustache Loader instance
|
||||
*/
|
||||
public function addLoader(Mustache_Loader $loader)
|
||||
{
|
||||
$this->loaders[] = $loader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a Template by name.
|
||||
*
|
||||
* @throws Mustache_Exception_UnknownTemplateException If a template file is not found.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return string Mustache Template source
|
||||
*/
|
||||
public function load($name)
|
||||
{
|
||||
foreach ($this->loaders as $loader) {
|
||||
try {
|
||||
return $loader->load($name);
|
||||
} catch (Mustache_Exception_UnknownTemplateException $e) {
|
||||
// do nothing, check the next loader.
|
||||
}
|
||||
}
|
||||
|
||||
throw new Mustache_Exception_UnknownTemplateException($name);
|
||||
}
|
||||
}
|
120
builder/lib/mustache/Loader/FilesystemLoader.php
Normal file
120
builder/lib/mustache/Loader/FilesystemLoader.php
Normal file
@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Mustache.php.
|
||||
*
|
||||
* (c) 2012 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Mustache Template filesystem Loader implementation.
|
||||
*
|
||||
* A FilesystemLoader instance loads Mustache Template source from the filesystem by name:
|
||||
*
|
||||
* $loader = new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views');
|
||||
* $tpl = $loader->load('foo'); // equivalent to `file_get_contents(dirname(__FILE__).'/views/foo.mustache');
|
||||
*
|
||||
* This is probably the most useful Mustache Loader implementation. It can be used for partials and normal Templates:
|
||||
*
|
||||
* $m = new Mustache(array(
|
||||
* 'loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views'),
|
||||
* 'partials_loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views/partials'),
|
||||
* ));
|
||||
*/
|
||||
class Mustache_Loader_FilesystemLoader implements Mustache_Loader
|
||||
{
|
||||
private $baseDir;
|
||||
private $extension = '.mustache';
|
||||
private $templates = array();
|
||||
|
||||
/**
|
||||
* Mustache filesystem Loader constructor.
|
||||
*
|
||||
* Passing an $options array allows overriding certain Loader options during instantiation:
|
||||
*
|
||||
* $options = array(
|
||||
* // The filename extension used for Mustache templates. Defaults to '.mustache'
|
||||
* 'extension' => '.ms',
|
||||
* );
|
||||
*
|
||||
* @throws Mustache_Exception_RuntimeException if $baseDir does not exist.
|
||||
*
|
||||
* @param string $baseDir Base directory containing Mustache template files.
|
||||
* @param array $options Array of Loader options (default: array())
|
||||
*/
|
||||
public function __construct($baseDir, array $options = array())
|
||||
{
|
||||
$this->baseDir = rtrim(realpath($baseDir), '/');
|
||||
|
||||
if (!is_dir($this->baseDir)) {
|
||||
throw new Mustache_Exception_RuntimeException(sprintf('FilesystemLoader baseDir must be a directory: %s', $baseDir));
|
||||
}
|
||||
|
||||
if (array_key_exists('extension', $options)) {
|
||||
if (empty($options['extension'])) {
|
||||
$this->extension = '';
|
||||
} else {
|
||||
$this->extension = '.' . ltrim($options['extension'], '.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a Template by name.
|
||||
*
|
||||
* $loader = new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views');
|
||||
* $loader->load('admin/dashboard'); // loads "./views/admin/dashboard.mustache";
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return string Mustache Template source
|
||||
*/
|
||||
public function load($name)
|
||||
{
|
||||
if (!isset($this->templates[$name])) {
|
||||
$this->templates[$name] = $this->loadFile($name);
|
||||
}
|
||||
|
||||
return $this->templates[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for loading a Mustache file by name.
|
||||
*
|
||||
* @throws Mustache_Exception_UnknownTemplateException If a template file is not found.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return string Mustache Template source
|
||||
*/
|
||||
protected function loadFile($name)
|
||||
{
|
||||
$fileName = $this->getFileName($name);
|
||||
|
||||
if (!file_exists($fileName)) {
|
||||
throw new Mustache_Exception_UnknownTemplateException($name);
|
||||
}
|
||||
|
||||
return file_get_contents($fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for getting a Mustache template file name.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return string Template file name
|
||||
*/
|
||||
protected function getFileName($name)
|
||||
{
|
||||
$fileName = $this->baseDir . '/' . $name;
|
||||
if (substr($fileName, 0 - strlen($this->extension)) !== $this->extension) {
|
||||
$fileName .= $this->extension;
|
||||
}
|
||||
|
||||
return $fileName;
|
||||
}
|
||||
}
|
121
builder/lib/mustache/Loader/InlineLoader.php
Normal file
121
builder/lib/mustache/Loader/InlineLoader.php
Normal file
@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Mustache.php.
|
||||
*
|
||||
* (c) 2013 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A Mustache Template loader for inline templates.
|
||||
*
|
||||
* With the InlineLoader, templates can be defined at the end of any PHP source
|
||||
* file:
|
||||
*
|
||||
* $loader = new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__);
|
||||
* $hello = $loader->load('hello');
|
||||
* $goodbye = $loader->load('goodbye');
|
||||
*
|
||||
* __halt_compiler();
|
||||
*
|
||||
* @@ hello
|
||||
* Hello, {{ planet }}!
|
||||
*
|
||||
* @@ goodbye
|
||||
* Goodbye, cruel {{ planet }}
|
||||
*
|
||||
* Templates are deliniated by lines containing only `@@ name`.
|
||||
*
|
||||
* The InlineLoader is well-suited to micro-frameworks such as Silex:
|
||||
*
|
||||
* $app->register(new MustacheServiceProvider, array(
|
||||
* 'mustache.loader' => new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__)
|
||||
* ));
|
||||
*
|
||||
* $app->get('/{name}', function() use ($app) {
|
||||
* return $app['mustache']->render('hello', compact('name'));
|
||||
* })
|
||||
* ->value('name', 'world');
|
||||
*
|
||||
* __halt_compiler();
|
||||
*
|
||||
* @@ hello
|
||||
* Hello, {{ name }}!
|
||||
*
|
||||
*/
|
||||
class Mustache_Loader_InlineLoader implements Mustache_Loader
|
||||
{
|
||||
protected $fileName;
|
||||
protected $offset;
|
||||
protected $templates;
|
||||
|
||||
/**
|
||||
* The InlineLoader requires a filename and offset to process templates.
|
||||
* The magic constants `__FILE__` and `__COMPILER_HALT_OFFSET__` are usually
|
||||
* perfectly suited to the job:
|
||||
*
|
||||
* $loader = new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__);
|
||||
*
|
||||
* Note that this only works if the loader is instantiated inside the same
|
||||
* file as the inline templates. If the templates are located in another
|
||||
* file, it would be necessary to manually specify the filename and offset.
|
||||
*
|
||||
* @param string $fileName The file to parse for inline templates
|
||||
* @param int $offset A string offset for the start of the templates.
|
||||
* This usually coincides with the `__halt_compiler`
|
||||
* call, and the `__COMPILER_HALT_OFFSET__`.
|
||||
*/
|
||||
public function __construct($fileName, $offset)
|
||||
{
|
||||
if (!is_file($fileName)) {
|
||||
throw new Mustache_Exception_InvalidArgumentException('InlineLoader expects a valid filename.');
|
||||
}
|
||||
|
||||
if (!is_int($offset) || $offset < 0) {
|
||||
throw new Mustache_Exception_InvalidArgumentException('InlineLoader expects a valid file offset.');
|
||||
}
|
||||
|
||||
$this->fileName = $fileName;
|
||||
$this->offset = $offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a Template by name.
|
||||
*
|
||||
* @throws Mustache_Exception_UnknownTemplateException If a template file is not found.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return string Mustache Template source
|
||||
*/
|
||||
public function load($name)
|
||||
{
|
||||
$this->loadTemplates();
|
||||
|
||||
if (!array_key_exists($name, $this->templates)) {
|
||||
throw new Mustache_Exception_UnknownTemplateException($name);
|
||||
}
|
||||
|
||||
return $this->templates[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and load templates from the end of a source file.
|
||||
*/
|
||||
protected function loadTemplates()
|
||||
{
|
||||
if ($this->templates === null) {
|
||||
$this->templates = array();
|
||||
$data = file_get_contents($this->fileName, false, null, $this->offset);
|
||||
foreach (preg_split("/^@@(?= [\w\d\.]+$)/m", $data, -1) as $chunk) {
|
||||
if (trim($chunk)) {
|
||||
list($name, $content) = explode("\n", $chunk, 2);
|
||||
$this->templates[trim($name)] = trim($content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
32
builder/lib/mustache/Loader/MutableLoader.php
Normal file
32
builder/lib/mustache/Loader/MutableLoader.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Mustache.php.
|
||||
*
|
||||
* (c) 2012 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Mustache Template mutable Loader interface.
|
||||
*/
|
||||
interface Mustache_Loader_MutableLoader
|
||||
{
|
||||
|
||||
/**
|
||||
* Set an associative array of Template sources for this loader.
|
||||
*
|
||||
* @param array $templates
|
||||
*/
|
||||
public function setTemplates(array $templates);
|
||||
|
||||
/**
|
||||
* Set a Template source by name.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $template Mustache Template source
|
||||
*/
|
||||
public function setTemplate($name, $template);
|
||||
}
|
123
builder/lib/mustache/Loader/PatternLoader.php
Normal file
123
builder/lib/mustache/Loader/PatternLoader.php
Normal file
@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Mustache.php.
|
||||
*
|
||||
* (c) 2012 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Mustache Template filesystem Loader implementation.
|
||||
*
|
||||
* A FilesystemLoader instance loads Mustache Template source from the filesystem by name:
|
||||
*
|
||||
* $loader = new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views');
|
||||
* $tpl = $loader->load('foo'); // equivalent to `file_get_contents(dirname(__FILE__).'/views/foo.mustache');
|
||||
*
|
||||
* This is probably the most useful Mustache Loader implementation. It can be used for partials and normal Templates:
|
||||
*
|
||||
* $m = new Mustache(array(
|
||||
* 'loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views'),
|
||||
* 'partials_loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views/partials'),
|
||||
* ));
|
||||
*/
|
||||
class Mustache_Loader_PatternLoader implements Mustache_Loader
|
||||
{
|
||||
private $baseDir;
|
||||
private $extension = '.mustache';
|
||||
private $templates = array();
|
||||
|
||||
/**
|
||||
* Mustache filesystem Loader constructor.
|
||||
*
|
||||
* Passing an $options array allows overriding certain Loader options during instantiation:
|
||||
*
|
||||
* $options = array(
|
||||
* // The filename extension used for Mustache templates. Defaults to '.mustache'
|
||||
* 'extension' => '.ms',
|
||||
* );
|
||||
*
|
||||
* @throws Mustache_Exception_RuntimeException if $baseDir does not exist.
|
||||
*
|
||||
* @param string $baseDir Base directory containing Mustache template files.
|
||||
* @param array $options Array of Loader options (default: array())
|
||||
*/
|
||||
public function __construct($baseDir, array $options = array())
|
||||
{
|
||||
$this->baseDir = rtrim(realpath($baseDir), '/');
|
||||
|
||||
if (!is_dir($this->baseDir)) {
|
||||
throw new Mustache_Exception_RuntimeException(sprintf('FilesystemLoader baseDir must be a directory: %s', $baseDir));
|
||||
}
|
||||
|
||||
if (array_key_exists('extension', $options)) {
|
||||
if (empty($options['extension'])) {
|
||||
$this->extension = '';
|
||||
} else {
|
||||
$this->extension = '.' . ltrim($options['extension'], '.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a Template by name.
|
||||
*
|
||||
* $loader = new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views');
|
||||
* $loader->load('admin/dashboard'); // loads "./views/admin/dashboard.mustache";
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return string Mustache Template source
|
||||
*/
|
||||
public function load($name)
|
||||
{
|
||||
if (!isset($this->templates[$name])) {
|
||||
try {
|
||||
$this->templates[$name] = $this->loadFile($name);
|
||||
return $this->templates[$name];
|
||||
} catch (Exception $e) {
|
||||
print "The partial, ".$name.", wasn't found so a pattern failed to build.\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for loading a Mustache file by name.
|
||||
*
|
||||
* @throws Mustache_Exception_UnknownTemplateException If a template file is not found.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return string Mustache Template source
|
||||
*/
|
||||
protected function loadFile($name)
|
||||
{
|
||||
$fileName = $this->getFileName($name);
|
||||
|
||||
if (!file_exists($fileName)) {
|
||||
throw new Mustache_Exception_UnknownTemplateException($name);
|
||||
}
|
||||
|
||||
return file_get_contents($fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for getting a Mustache template file name.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return string Template file name
|
||||
*/
|
||||
protected function getFileName($name)
|
||||
{
|
||||
$fileName = $this->baseDir . '/' . $name;
|
||||
if (substr($fileName, 0 - strlen($this->extension)) !== $this->extension) {
|
||||
preg_match('/\/(([amotp])\-([A-z0-9]{1,})\-([A-z0-9-]{1,}))$/',$fileName,$matches);
|
||||
$fileName .= "/".$matches[1].$this->extension;
|
||||
}
|
||||
return $fileName;
|
||||
}
|
||||
}
|
40
builder/lib/mustache/Loader/StringLoader.php
Normal file
40
builder/lib/mustache/Loader/StringLoader.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Mustache.php.
|
||||
*
|
||||
* (c) 2012 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Mustache Template string Loader implementation.
|
||||
*
|
||||
* A StringLoader instance is essentially a noop. It simply passes the 'name' argument straight through:
|
||||
*
|
||||
* $loader = new StringLoader;
|
||||
* $tpl = $loader->load('{{ foo }}'); // '{{ foo }}'
|
||||
*
|
||||
* This is the default Template Loader instance used by Mustache:
|
||||
*
|
||||
* $m = new Mustache;
|
||||
* $tpl = $m->loadTemplate('{{ foo }}');
|
||||
* echo $tpl->render(array('foo' => 'bar')); // "bar"
|
||||
*/
|
||||
class Mustache_Loader_StringLoader implements Mustache_Loader
|
||||
{
|
||||
|
||||
/**
|
||||
* Load a Template by source.
|
||||
*
|
||||
* @param string $name Mustache Template source
|
||||
*
|
||||
* @return string Mustache Template source
|
||||
*/
|
||||
public function load($name)
|
||||
{
|
||||
return $name;
|
||||
}
|
||||
}
|
135
builder/lib/mustache/Logger.php
Normal file
135
builder/lib/mustache/Logger.php
Normal file
@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Mustache.php.
|
||||
*
|
||||
* (c) 2012 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Describes a Mustache logger instance
|
||||
*
|
||||
* This is identical to the Psr\Log\LoggerInterface.
|
||||
*
|
||||
* The message MUST be a string or object implementing __toString().
|
||||
*
|
||||
* The message MAY contain placeholders in the form: {foo} where foo
|
||||
* will be replaced by the context data in key "foo".
|
||||
*
|
||||
* The context array can contain arbitrary data, the only assumption that
|
||||
* can be made by implementors is that if an Exception instance is given
|
||||
* to produce a stack trace, it MUST be in a key named "exception".
|
||||
*
|
||||
* See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md
|
||||
* for the full interface specification.
|
||||
*/
|
||||
interface Mustache_Logger
|
||||
{
|
||||
/**
|
||||
* Psr\Log compatible log levels
|
||||
*/
|
||||
const EMERGENCY = 'emergency';
|
||||
const ALERT = 'alert';
|
||||
const CRITICAL = 'critical';
|
||||
const ERROR = 'error';
|
||||
const WARNING = 'warning';
|
||||
const NOTICE = 'notice';
|
||||
const INFO = 'info';
|
||||
const DEBUG = 'debug';
|
||||
|
||||
/**
|
||||
* System is unusable.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
* @return null
|
||||
*/
|
||||
public function emergency($message, array $context = array());
|
||||
|
||||
/**
|
||||
* Action must be taken immediately.
|
||||
*
|
||||
* Example: Entire website down, database unavailable, etc. This should
|
||||
* trigger the SMS alerts and wake you up.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
* @return null
|
||||
*/
|
||||
public function alert($message, array $context = array());
|
||||
|
||||
/**
|
||||
* Critical conditions.
|
||||
*
|
||||
* Example: Application component unavailable, unexpected exception.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
* @return null
|
||||
*/
|
||||
public function critical($message, array $context = array());
|
||||
|
||||
/**
|
||||
* Runtime errors that do not require immediate action but should typically
|
||||
* be logged and monitored.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
* @return null
|
||||
*/
|
||||
public function error($message, array $context = array());
|
||||
|
||||
/**
|
||||
* Exceptional occurrences that are not errors.
|
||||
*
|
||||
* Example: Use of deprecated APIs, poor use of an API, undesirable things
|
||||
* that are not necessarily wrong.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
* @return null
|
||||
*/
|
||||
public function warning($message, array $context = array());
|
||||
|
||||
/**
|
||||
* Normal but significant events.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
* @return null
|
||||
*/
|
||||
public function notice($message, array $context = array());
|
||||
|
||||
/**
|
||||
* Interesting events.
|
||||
*
|
||||
* Example: User logs in, SQL logs.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
* @return null
|
||||
*/
|
||||
public function info($message, array $context = array());
|
||||
|
||||
/**
|
||||
* Detailed debug information.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
* @return null
|
||||
*/
|
||||
public function debug($message, array $context = array());
|
||||
|
||||
/**
|
||||
* Logs with an arbitrary level.
|
||||
*
|
||||
* @param mixed $level
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
* @return null
|
||||
*/
|
||||
public function log($level, $message, array $context = array());
|
||||
}
|
121
builder/lib/mustache/Logger/AbstractLogger.php
Normal file
121
builder/lib/mustache/Logger/AbstractLogger.php
Normal file
@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Mustache.php.
|
||||
*
|
||||
* (c) 2012 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This is a simple Logger implementation that other Loggers can inherit from.
|
||||
*
|
||||
* This is identical to the Psr\Log\AbstractLogger.
|
||||
*
|
||||
* It simply delegates all log-level-specific methods to the `log` method to
|
||||
* reduce boilerplate code that a simple Logger that does the same thing with
|
||||
* messages regardless of the error level has to implement.
|
||||
*/
|
||||
abstract class Mustache_Logger_AbstractLogger implements Mustache_Logger
|
||||
{
|
||||
/**
|
||||
* System is unusable.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
*/
|
||||
public function emergency($message, array $context = array())
|
||||
{
|
||||
$this->log(Mustache_Logger::EMERGENCY, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Action must be taken immediately.
|
||||
*
|
||||
* Example: Entire website down, database unavailable, etc. This should
|
||||
* trigger the SMS alerts and wake you up.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
*/
|
||||
public function alert($message, array $context = array())
|
||||
{
|
||||
$this->log(Mustache_Logger::ALERT, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Critical conditions.
|
||||
*
|
||||
* Example: Application component unavailable, unexpected exception.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
*/
|
||||
public function critical($message, array $context = array())
|
||||
{
|
||||
$this->log(Mustache_Logger::CRITICAL, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime errors that do not require immediate action but should typically
|
||||
* be logged and monitored.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
*/
|
||||
public function error($message, array $context = array())
|
||||
{
|
||||
$this->log(Mustache_Logger::ERROR, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exceptional occurrences that are not errors.
|
||||
*
|
||||
* Example: Use of deprecated APIs, poor use of an API, undesirable things
|
||||
* that are not necessarily wrong.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
*/
|
||||
public function warning($message, array $context = array())
|
||||
{
|
||||
$this->log(Mustache_Logger::WARNING, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normal but significant events.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
*/
|
||||
public function notice($message, array $context = array())
|
||||
{
|
||||
$this->log(Mustache_Logger::NOTICE, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interesting events.
|
||||
*
|
||||
* Example: User logs in, SQL logs.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
*/
|
||||
public function info($message, array $context = array())
|
||||
{
|
||||
$this->log(Mustache_Logger::INFO, $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detailed debug information.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
*/
|
||||
public function debug($message, array $context = array())
|
||||
{
|
||||
$this->log(Mustache_Logger::DEBUG, $message, $context);
|
||||
}
|
||||
}
|
193
builder/lib/mustache/Logger/StreamLogger.php
Normal file
193
builder/lib/mustache/Logger/StreamLogger.php
Normal file
@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Mustache.php.
|
||||
*
|
||||
* (c) 2012 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A Mustache Stream Logger.
|
||||
*
|
||||
* The Stream Logger wraps a file resource instance (such as a stream) or a
|
||||
* stream URL. All log messages over the threshold level will be appended to
|
||||
* this stream.
|
||||
*
|
||||
* Hint: Try `php://stderr` for your stream URL.
|
||||
*/
|
||||
class Mustache_Logger_StreamLogger extends Mustache_Logger_AbstractLogger
|
||||
{
|
||||
protected static $levels = array(
|
||||
self::DEBUG => 100,
|
||||
self::INFO => 200,
|
||||
self::NOTICE => 250,
|
||||
self::WARNING => 300,
|
||||
self::ERROR => 400,
|
||||
self::CRITICAL => 500,
|
||||
self::ALERT => 550,
|
||||
self::EMERGENCY => 600,
|
||||
);
|
||||
|
||||
protected $stream = null;
|
||||
protected $url = null;
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException if the logging level is unknown.
|
||||
*
|
||||
* @param string $stream Resource instance or URL
|
||||
* @param integer $level The minimum logging level at which this handler will be triggered
|
||||
*/
|
||||
public function __construct($stream, $level = Mustache_Logger::ERROR)
|
||||
{
|
||||
$this->setLevel($level);
|
||||
|
||||
if (is_resource($stream)) {
|
||||
$this->stream = $stream;
|
||||
} else {
|
||||
$this->url = $stream;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close stream resources.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if (is_resource($this->stream)) {
|
||||
fclose($this->stream);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the minimum logging level.
|
||||
*
|
||||
* @throws Mustache_Exception_InvalidArgumentException if the logging level is unknown.
|
||||
*
|
||||
* @param integer $level The minimum logging level which will be written
|
||||
*/
|
||||
public function setLevel($level)
|
||||
{
|
||||
if (!array_key_exists($level, self::$levels)) {
|
||||
throw new Mustache_Exception_InvalidArgumentException(sprintf('Unexpected logging level: %s', $level));
|
||||
}
|
||||
|
||||
$this->level = $level;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current minimum logging level.
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getLevel()
|
||||
{
|
||||
return $this->level;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs with an arbitrary level.
|
||||
*
|
||||
* @throws Mustache_Exception_InvalidArgumentException if the logging level is unknown.
|
||||
*
|
||||
* @param mixed $level
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
*/
|
||||
public function log($level, $message, array $context = array())
|
||||
{
|
||||
if (!array_key_exists($level, self::$levels)) {
|
||||
throw new Mustache_Exception_InvalidArgumentException(sprintf('Unexpected logging level: %s', $level));
|
||||
}
|
||||
|
||||
if (self::$levels[$level] >= self::$levels[$this->level]) {
|
||||
$this->writeLog($level, $message, $context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a record to the log.
|
||||
*
|
||||
* @throws Mustache_Exception_LogicException If neither a stream resource nor url is present.
|
||||
* @throws Mustache_Exception_RuntimeException If the stream url cannot be opened.
|
||||
*
|
||||
* @param integer $level The logging level
|
||||
* @param string $message The log message
|
||||
* @param array $context The log context
|
||||
*/
|
||||
protected function writeLog($level, $message, array $context = array())
|
||||
{
|
||||
if (!is_resource($this->stream)) {
|
||||
if (!isset($this->url)) {
|
||||
throw new Mustache_Exception_LogicException('Missing stream url, the stream can not be opened. This may be caused by a premature call to close().');
|
||||
}
|
||||
|
||||
$this->stream = fopen($this->url, 'a');
|
||||
if (!is_resource($this->stream)) {
|
||||
// @codeCoverageIgnoreStart
|
||||
throw new Mustache_Exception_RuntimeException(sprintf('The stream or file "%s" could not be opened.', $this->url));
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
}
|
||||
|
||||
fwrite($this->stream, self::formatLine($level, $message, $context));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of the logging level.
|
||||
*
|
||||
* @throws InvalidArgumentException if the logging level is unknown.
|
||||
*
|
||||
* @param integer $level
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function getLevelName($level)
|
||||
{
|
||||
return strtoupper($level);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a log line for output.
|
||||
*
|
||||
* @param integer $level The logging level
|
||||
* @param string $message The log message
|
||||
* @param array $context The log context
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function formatLine($level, $message, array $context = array())
|
||||
{
|
||||
return sprintf(
|
||||
"%s: %s\n",
|
||||
self::getLevelName($level),
|
||||
self::interpolateMessage($message, $context)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolate context values into the message placeholders.
|
||||
*
|
||||
* @param string $message
|
||||
* @param array $context
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function interpolateMessage($message, array $context = array())
|
||||
{
|
||||
if (strpos($message, '{') === false) {
|
||||
return $message;
|
||||
}
|
||||
|
||||
// build a replacement array with braces around the context keys
|
||||
$replace = array();
|
||||
foreach ($context as $key => $val) {
|
||||
$replace['{' . $key . '}'] = $val;
|
||||
}
|
||||
|
||||
// interpolate replacement values into the the message and return
|
||||
return strtr($message, $replace);
|
||||
}
|
||||
}
|
91
builder/lib/mustache/Parser.php
Normal file
91
builder/lib/mustache/Parser.php
Normal file
@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Mustache.php.
|
||||
*
|
||||
* (c) 2012 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Mustache Parser class.
|
||||
*
|
||||
* This class is responsible for turning a set of Mustache tokens into a parse tree.
|
||||
*/
|
||||
class Mustache_Parser
|
||||
{
|
||||
|
||||
/**
|
||||
* Process an array of Mustache tokens and convert them into a parse tree.
|
||||
*
|
||||
* @param array $tokens Set of Mustache tokens
|
||||
*
|
||||
* @return array Mustache token parse tree
|
||||
*/
|
||||
public function parse(array $tokens = array())
|
||||
{
|
||||
return $this->buildTree(new ArrayIterator($tokens));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for recursively building a parse tree.
|
||||
*
|
||||
* @throws Mustache_Exception_SyntaxException when nesting errors or mismatched section tags are encountered.
|
||||
*
|
||||
* @param ArrayIterator $tokens Stream of Mustache tokens
|
||||
* @param array $parent Parent token (default: null)
|
||||
*
|
||||
* @return array Mustache Token parse tree
|
||||
*/
|
||||
private function buildTree(ArrayIterator $tokens, array $parent = null)
|
||||
{
|
||||
$nodes = array();
|
||||
|
||||
do {
|
||||
$token = $tokens->current();
|
||||
$tokens->next();
|
||||
|
||||
if ($token === null) {
|
||||
continue;
|
||||
} else {
|
||||
switch ($token[Mustache_Tokenizer::TYPE]) {
|
||||
case Mustache_Tokenizer::T_SECTION:
|
||||
case Mustache_Tokenizer::T_INVERTED:
|
||||
$nodes[] = $this->buildTree($tokens, $token);
|
||||
break;
|
||||
|
||||
case Mustache_Tokenizer::T_END_SECTION:
|
||||
if (!isset($parent)) {
|
||||
$msg = sprintf('Unexpected closing tag: /%s', $token[Mustache_Tokenizer::NAME]);
|
||||
throw new Mustache_Exception_SyntaxException($msg, $token);
|
||||
}
|
||||
|
||||
if ($token[Mustache_Tokenizer::NAME] !== $parent[Mustache_Tokenizer::NAME]) {
|
||||
$msg = sprintf('Nesting error: %s vs. %s', $parent[Mustache_Tokenizer::NAME], $token[Mustache_Tokenizer::NAME]);
|
||||
throw new Mustache_Exception_SyntaxException($msg, $token);
|
||||
}
|
||||
|
||||
$parent[Mustache_Tokenizer::END] = $token[Mustache_Tokenizer::INDEX];
|
||||
$parent[Mustache_Tokenizer::NODES] = $nodes;
|
||||
|
||||
return $parent;
|
||||
break;
|
||||
|
||||
default:
|
||||
$nodes[] = $token;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} while ($tokens->valid());
|
||||
|
||||
if (isset($parent)) {
|
||||
$msg = sprintf('Missing closing tag: %s', $parent[Mustache_Tokenizer::NAME]);
|
||||
throw new Mustache_Exception_SyntaxException($msg, $parent);
|
||||
}
|
||||
|
||||
return $nodes;
|
||||
}
|
||||
}
|
177
builder/lib/mustache/Template.php
Normal file
177
builder/lib/mustache/Template.php
Normal file
@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Mustache.php.
|
||||
*
|
||||
* (c) 2012 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Abstract Mustache Template class.
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
abstract class Mustache_Template
|
||||
{
|
||||
|
||||
/**
|
||||
* @var Mustache_Engine
|
||||
*/
|
||||
protected $mustache;
|
||||
|
||||
/**
|
||||
* @var boolean
|
||||
*/
|
||||
protected $strictCallables = false;
|
||||
|
||||
/**
|
||||
* Mustache Template constructor.
|
||||
*
|
||||
* @param Mustache_Engine $mustache
|
||||
*/
|
||||
public function __construct(Mustache_Engine $mustache)
|
||||
{
|
||||
$this->mustache = $mustache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mustache Template instances can be treated as a function and rendered by simply calling them:
|
||||
*
|
||||
* $m = new Mustache_Engine;
|
||||
* $tpl = $m->loadTemplate('Hello, {{ name }}!');
|
||||
* echo $tpl(array('name' => 'World')); // "Hello, World!"
|
||||
*
|
||||
* @see Mustache_Template::render
|
||||
*
|
||||
* @param mixed $context Array or object rendering context (default: array())
|
||||
*
|
||||
* @return string Rendered template
|
||||
*/
|
||||
public function __invoke($context = array())
|
||||
{
|
||||
return $this->render($context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render this template given the rendering context.
|
||||
*
|
||||
* @param mixed $context Array or object rendering context (default: array())
|
||||
*
|
||||
* @return string Rendered template
|
||||
*/
|
||||
public function render($context = array())
|
||||
{
|
||||
return $this->renderInternal($this->prepareContextStack($context));
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal rendering method implemented by Mustache Template concrete subclasses.
|
||||
*
|
||||
* This is where the magic happens :)
|
||||
*
|
||||
* NOTE: This method is not part of the Mustache.php public API.
|
||||
*
|
||||
* @param Mustache_Context $context
|
||||
* @param string $indent (default: '')
|
||||
*
|
||||
* @return string Rendered template
|
||||
*/
|
||||
abstract public function renderInternal(Mustache_Context $context, $indent = '');
|
||||
|
||||
/**
|
||||
* Tests whether a value should be iterated over (e.g. in a section context).
|
||||
*
|
||||
* In most languages there are two distinct array types: list and hash (or whatever you want to call them). Lists
|
||||
* should be iterated, hashes should be treated as objects. Mustache follows this paradigm for Ruby, Javascript,
|
||||
* Java, Python, etc.
|
||||
*
|
||||
* PHP, however, treats lists and hashes as one primitive type: array. So Mustache.php needs a way to distinguish
|
||||
* between between a list of things (numeric, normalized array) and a set of variables to be used as section context
|
||||
* (associative array). In other words, this will be iterated over:
|
||||
*
|
||||
* $items = array(
|
||||
* array('name' => 'foo'),
|
||||
* array('name' => 'bar'),
|
||||
* array('name' => 'baz'),
|
||||
* );
|
||||
*
|
||||
* ... but this will be used as a section context block:
|
||||
*
|
||||
* $items = array(
|
||||
* 1 => array('name' => 'foo'),
|
||||
* 'banana' => array('name' => 'bar'),
|
||||
* 42 => array('name' => 'baz'),
|
||||
* );
|
||||
*
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return boolean True if the value is 'iterable'
|
||||
*/
|
||||
protected function isIterable($value)
|
||||
{
|
||||
if (is_object($value)) {
|
||||
return $value instanceof Traversable;
|
||||
} elseif (is_array($value)) {
|
||||
$i = 0;
|
||||
foreach ($value as $k => $v) {
|
||||
if ($k !== $i++) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to prepare the Context stack.
|
||||
*
|
||||
* Adds the Mustache HelperCollection to the stack's top context frame if helpers are present.
|
||||
*
|
||||
* @param mixed $context Optional first context frame (default: null)
|
||||
*
|
||||
* @return Mustache_Context
|
||||
*/
|
||||
protected function prepareContextStack($context = null)
|
||||
{
|
||||
$stack = new Mustache_Context;
|
||||
|
||||
$helpers = $this->mustache->getHelpers();
|
||||
if (!$helpers->isEmpty()) {
|
||||
$stack->push($helpers);
|
||||
}
|
||||
|
||||
if (!empty($context)) {
|
||||
$stack->push($context);
|
||||
}
|
||||
|
||||
return $stack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a context value.
|
||||
*
|
||||
* Invoke the value if it is callable, otherwise return the value.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param Mustache_Context $context
|
||||
* @param string $indent
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function resolveValue($value, Mustache_Context $context, $indent = '')
|
||||
{
|
||||
if (($this->strictCallables ? is_object($value) : !is_string($value)) && is_callable($value)) {
|
||||
return $this->mustache
|
||||
->loadLambda((string) call_user_func($value))
|
||||
->renderInternal($context, $indent);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
315
builder/lib/mustache/Tokenizer.php
Normal file
315
builder/lib/mustache/Tokenizer.php
Normal file
@ -0,0 +1,315 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Mustache.php.
|
||||
*
|
||||
* (c) 2012 Justin Hileman
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Mustache Tokenizer class.
|
||||
*
|
||||
* This class is responsible for turning raw template source into a set of Mustache tokens.
|
||||
*/
|
||||
class Mustache_Tokenizer
|
||||
{
|
||||
|
||||
// Finite state machine states
|
||||
const IN_TEXT = 0;
|
||||
const IN_TAG_TYPE = 1;
|
||||
const IN_TAG = 2;
|
||||
|
||||
// Token types
|
||||
const T_SECTION = '#';
|
||||
const T_INVERTED = '^';
|
||||
const T_END_SECTION = '/';
|
||||
const T_COMMENT = '!';
|
||||
const T_PARTIAL = '>';
|
||||
const T_PARTIAL_2 = '<';
|
||||
const T_DELIM_CHANGE = '=';
|
||||
const T_ESCAPED = '_v';
|
||||
const T_UNESCAPED = '{';
|
||||
const T_UNESCAPED_2 = '&';
|
||||
const T_TEXT = '_t';
|
||||
const T_PRAGMA = '%';
|
||||
|
||||
// Valid token types
|
||||
private static $tagTypes = array(
|
||||
self::T_SECTION => true,
|
||||
self::T_INVERTED => true,
|
||||
self::T_END_SECTION => true,
|
||||
self::T_COMMENT => true,
|
||||
self::T_PARTIAL => true,
|
||||
self::T_PARTIAL_2 => true,
|
||||
self::T_DELIM_CHANGE => true,
|
||||
self::T_ESCAPED => true,
|
||||
self::T_UNESCAPED => true,
|
||||
self::T_UNESCAPED_2 => true,
|
||||
self::T_PRAGMA => true,
|
||||
);
|
||||
|
||||
// Interpolated tags
|
||||
private static $interpolatedTags = array(
|
||||
self::T_ESCAPED => true,
|
||||
self::T_UNESCAPED => true,
|
||||
self::T_UNESCAPED_2 => true,
|
||||
);
|
||||
|
||||
// Token properties
|
||||
const TYPE = 'type';
|
||||
const NAME = 'name';
|
||||
const OTAG = 'otag';
|
||||
const CTAG = 'ctag';
|
||||
const INDEX = 'index';
|
||||
const END = 'end';
|
||||
const INDENT = 'indent';
|
||||
const NODES = 'nodes';
|
||||
const VALUE = 'value';
|
||||
|
||||
private $pragmas;
|
||||
private $state;
|
||||
private $tagType;
|
||||
private $tag;
|
||||
private $buffer;
|
||||
private $tokens;
|
||||
private $seenTag;
|
||||
private $lineStart;
|
||||
private $otag;
|
||||
private $ctag;
|
||||
|
||||
/**
|
||||
* Scan and tokenize template source.
|
||||
*
|
||||
* @param string $text Mustache template source to tokenize
|
||||
* @param string $delimiters Optionally, pass initial opening and closing delimiters (default: null)
|
||||
*
|
||||
* @return array Set of Mustache tokens
|
||||
*/
|
||||
public function scan($text, $delimiters = null)
|
||||
{
|
||||
$this->reset();
|
||||
|
||||
if ($delimiters = trim($delimiters)) {
|
||||
list($otag, $ctag) = explode(' ', $delimiters);
|
||||
$this->otag = $otag;
|
||||
$this->ctag = $ctag;
|
||||
}
|
||||
|
||||
$len = strlen($text);
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
switch ($this->state) {
|
||||
case self::IN_TEXT:
|
||||
if ($this->tagChange($this->otag, $text, $i)) {
|
||||
$i--;
|
||||
$this->flushBuffer();
|
||||
$this->state = self::IN_TAG_TYPE;
|
||||
} else {
|
||||
$char = substr($text, $i, 1);
|
||||
if ($char == "\n") {
|
||||
$this->filterLine();
|
||||
} else {
|
||||
$this->buffer .= $char;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case self::IN_TAG_TYPE:
|
||||
|
||||
$i += strlen($this->otag) - 1;
|
||||
$char = substr($text, $i + 1, 1);
|
||||
if (isset(self::$tagTypes[$char])) {
|
||||
$tag = $char;
|
||||
$this->tagType = $tag;
|
||||
} else {
|
||||
$tag = null;
|
||||
$this->tagType = self::T_ESCAPED;
|
||||
}
|
||||
|
||||
if ($this->tagType === self::T_DELIM_CHANGE) {
|
||||
$i = $this->changeDelimiters($text, $i);
|
||||
$this->state = self::IN_TEXT;
|
||||
} elseif ($this->tagType === self::T_PRAGMA) {
|
||||
$i = $this->addPragma($text, $i);
|
||||
$this->state = self::IN_TEXT;
|
||||
} else {
|
||||
if ($tag !== null) {
|
||||
$i++;
|
||||
}
|
||||
$this->state = self::IN_TAG;
|
||||
}
|
||||
$this->seenTag = $i;
|
||||
break;
|
||||
|
||||
default:
|
||||
if ($this->tagChange($this->ctag, $text, $i)) {
|
||||
$this->tokens[] = array(
|
||||
self::TYPE => $this->tagType,
|
||||
self::NAME => trim($this->buffer),
|
||||
self::OTAG => $this->otag,
|
||||
self::CTAG => $this->ctag,
|
||||
self::INDEX => ($this->tagType == self::T_END_SECTION) ? $this->seenTag - strlen($this->otag) : $i + strlen($this->ctag)
|
||||
);
|
||||
|
||||
$this->buffer = '';
|
||||
$i += strlen($this->ctag) - 1;
|
||||
$this->state = self::IN_TEXT;
|
||||
if ($this->tagType == self::T_UNESCAPED) {
|
||||
if ($this->ctag == '}}') {
|
||||
$i++;
|
||||
} else {
|
||||
// Clean up `{{{ tripleStache }}}` style tokens.
|
||||
$lastName = $this->tokens[count($this->tokens) - 1][self::NAME];
|
||||
if (substr($lastName, -1) === '}') {
|
||||
$this->tokens[count($this->tokens) - 1][self::NAME] = trim(substr($lastName, 0, -1));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->buffer .= substr($text, $i, 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->filterLine(true);
|
||||
|
||||
foreach ($this->pragmas as $pragma) {
|
||||
array_unshift($this->tokens, array(
|
||||
self::TYPE => self::T_PRAGMA,
|
||||
self::NAME => $pragma,
|
||||
));
|
||||
}
|
||||
|
||||
return $this->tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to reset tokenizer internal state.
|
||||
*/
|
||||
private function reset()
|
||||
{
|
||||
$this->state = self::IN_TEXT;
|
||||
$this->tagType = null;
|
||||
$this->tag = null;
|
||||
$this->buffer = '';
|
||||
$this->tokens = array();
|
||||
$this->seenTag = false;
|
||||
$this->lineStart = 0;
|
||||
$this->otag = '{{';
|
||||
$this->ctag = '}}';
|
||||
$this->pragmas = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush the current buffer to a token.
|
||||
*/
|
||||
private function flushBuffer()
|
||||
{
|
||||
if (!empty($this->buffer)) {
|
||||
$this->tokens[] = array(self::TYPE => self::T_TEXT, self::VALUE => $this->buffer);
|
||||
$this->buffer = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the current line is entirely made up of whitespace.
|
||||
*
|
||||
* @return boolean True if the current line is all whitespace
|
||||
*/
|
||||
private function lineIsWhitespace()
|
||||
{
|
||||
$tokensCount = count($this->tokens);
|
||||
for ($j = $this->lineStart; $j < $tokensCount; $j++) {
|
||||
$token = $this->tokens[$j];
|
||||
if (isset(self::$tagTypes[$token[self::TYPE]])) {
|
||||
if (isset(self::$interpolatedTags[$token[self::TYPE]])) {
|
||||
return false;
|
||||
}
|
||||
} elseif ($token[self::TYPE] == self::T_TEXT) {
|
||||
if (preg_match('/\S/', $token[self::VALUE])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter out whitespace-only lines and store indent levels for partials.
|
||||
*
|
||||
* @param bool $noNewLine Suppress the newline? (default: false)
|
||||
*/
|
||||
private function filterLine($noNewLine = false)
|
||||
{
|
||||
$this->flushBuffer();
|
||||
if ($this->seenTag && $this->lineIsWhitespace()) {
|
||||
$tokensCount = count($this->tokens);
|
||||
for ($j = $this->lineStart; $j < $tokensCount; $j++) {
|
||||
if ($this->tokens[$j][self::TYPE] == self::T_TEXT) {
|
||||
if (isset($this->tokens[$j+1]) && $this->tokens[$j+1][self::TYPE] == self::T_PARTIAL) {
|
||||
$this->tokens[$j+1][self::INDENT] = $this->tokens[$j][self::VALUE];
|
||||
}
|
||||
|
||||
$this->tokens[$j] = null;
|
||||
}
|
||||
}
|
||||
} elseif (!$noNewLine) {
|
||||
$this->tokens[] = array(self::TYPE => self::T_TEXT, self::VALUE => "\n");
|
||||
}
|
||||
|
||||
$this->seenTag = false;
|
||||
$this->lineStart = count($this->tokens);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the current Mustache delimiters. Set new `otag` and `ctag` values.
|
||||
*
|
||||
* @param string $text Mustache template source
|
||||
* @param int $index Current tokenizer index
|
||||
*
|
||||
* @return int New index value
|
||||
*/
|
||||
private function changeDelimiters($text, $index)
|
||||
{
|
||||
$startIndex = strpos($text, '=', $index) + 1;
|
||||
$close = '='.$this->ctag;
|
||||
$closeIndex = strpos($text, $close, $index);
|
||||
|
||||
list($otag, $ctag) = explode(' ', trim(substr($text, $startIndex, $closeIndex - $startIndex)));
|
||||
$this->otag = $otag;
|
||||
$this->ctag = $ctag;
|
||||
|
||||
return $closeIndex + strlen($close) - 1;
|
||||
}
|
||||
|
||||
private function addPragma($text, $index)
|
||||
{
|
||||
$end = strpos($text, $this->ctag, $index);
|
||||
$this->pragmas[] = trim(substr($text, $index + 2, $end - $index - 2));
|
||||
|
||||
return $end + strlen($this->ctag) - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether it's time to change tags.
|
||||
*
|
||||
* @param string $tag Current tag name
|
||||
* @param string $text Mustache template source
|
||||
* @param int $index Current tokenizer index
|
||||
*
|
||||
* @return boolean True if this is a closing section tag
|
||||
*/
|
||||
private function tagChange($tag, $text, $index)
|
||||
{
|
||||
return substr($text, $index, strlen($tag)) === $tag;
|
||||
}
|
||||
|
||||
public function returnTokens()
|
||||
{
|
||||
return $this->tokens;
|
||||
}
|
||||
}
|
188
builder/lib/watcher.lib.php
Normal file
188
builder/lib/watcher.lib.php
Normal file
@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
/*!
|
||||
* Pattern Lab Watcher Class - v0.1
|
||||
*
|
||||
* Copyright (c) 2013 Dave Olsen, http://dmolsen.com
|
||||
* Licensed under the MIT license
|
||||
*
|
||||
* Watches the source/patterns dir for any changes so they can be automagically
|
||||
* moved to the public/patterns dir.
|
||||
*
|
||||
* This is not the most efficient implementation of a directory watch but I hope
|
||||
* it's the most platform agnostic.
|
||||
*
|
||||
*/
|
||||
|
||||
class Watcher extends Builder {
|
||||
|
||||
/**
|
||||
* Use the Builder __construct to gather the config variables
|
||||
*/
|
||||
public function __construct() {
|
||||
|
||||
// construct the parent
|
||||
parent::__construct();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch the source directory for any changes to existing files. Will run forever if given the chance
|
||||
*/
|
||||
public function watch() {
|
||||
|
||||
$c = false; // have the files been added to the overall object?
|
||||
$t = false; // was their a change found? re-render
|
||||
$k = false; // was the entry not a part of the $o object? make sure it's hashes are added
|
||||
$m = false; // does the index page need to be regenerated?
|
||||
$o = new stdClass(); // create an object to hold the properties
|
||||
|
||||
// run forever
|
||||
while (true) {
|
||||
|
||||
// generate all of the patterns
|
||||
$entries = scandir(__DIR__.$this->sp);
|
||||
|
||||
foreach($entries as $entry) {
|
||||
|
||||
if (!in_array($entry,$this->if)) {
|
||||
|
||||
// figure out how to watch for new directories and new files
|
||||
if (!isset($o->$entry)) {
|
||||
$o->$entry = new stdClass();
|
||||
$k = true;
|
||||
}
|
||||
|
||||
// figure out the md5 hash of a file so we can track changes
|
||||
// runs well on a solid state drive. no idea if it thrashes regular disks
|
||||
$ph = $this->md5File(__DIR__.$this->sp.$entry."/".$entry.".mustache");
|
||||
$dh = $this->md5File(__DIR__.$this->sp.$entry."/data.json");
|
||||
|
||||
// if the directory wasn't being checked already add the md5 sums
|
||||
if ($k) {
|
||||
|
||||
$o->$entry->ph = $ph;
|
||||
$o->$entry->dh = $dh;
|
||||
|
||||
// if we're through the first check make sure to note any new directories being added to Pattern Lab
|
||||
// assuming a pattern actually exists
|
||||
if ($c && ($o->$entry->ph != '')) {
|
||||
print $entry."/".$entry.".mustache added to Pattern Lab...\n";
|
||||
$t = true;
|
||||
$m = true;
|
||||
}
|
||||
|
||||
$k = false;
|
||||
|
||||
} else {
|
||||
|
||||
if ($o->$entry->ph != $ph) {
|
||||
|
||||
if ($c && ($o->$entry->ph == '')) {
|
||||
print $entry."/".$entry.".mustache added to Pattern Lab...\n";
|
||||
$m = true;
|
||||
} else {
|
||||
print $entry."/".$entry.".mustache changed...\n";
|
||||
}
|
||||
|
||||
$t = true;
|
||||
$o->$entry->ph = $ph;
|
||||
|
||||
}
|
||||
|
||||
if ($o->$entry->dh != $dh) {
|
||||
$t = true;
|
||||
$o->$entry->dh = $dh;
|
||||
print $entry."/data.json changed...\n";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// if a file has been added or changed then render & move the *entire* project (shakes fist at partials)
|
||||
// if a new directory was added regenerate the main pages
|
||||
// also update the change time so that content sync will work properly
|
||||
if ($t) {
|
||||
$this->gatherData();
|
||||
$this->renderAndMove();
|
||||
$this->generateViewAllPages();
|
||||
$this->updateChangeTime();
|
||||
if ($m) {
|
||||
$this->generateMainPages();
|
||||
$m = false;
|
||||
}
|
||||
$t = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// check the user-supplied watch files (e.g. css)
|
||||
$i = 0;
|
||||
foreach($this->wf as $wf) {
|
||||
|
||||
if (!isset($o->$wf)) {
|
||||
$o->$wf = new stdClass();
|
||||
}
|
||||
|
||||
// md5 hash the user-supplied filenames, if it's changed just move the single file
|
||||
// update the change time so that content sync will work properly
|
||||
$fh = $this->md5File(__DIR__."/../../../source".$wf);
|
||||
if (!isset($o->$wf->fh)) {
|
||||
$o->$wf->fh = $fh;
|
||||
} else {
|
||||
if ($o->$wf->fh != $fh) {
|
||||
$o->$wf->fh = $fh;
|
||||
$this->moveFile($wf,$this->mf[$i]);
|
||||
$this->updateChangeTime();
|
||||
print $wf." changed...\n";
|
||||
};
|
||||
$i++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// check the main data.json file for changes, if it's changed render & move the *entire* project
|
||||
// update the change time so that content sync will work properly
|
||||
$dh = $this->md5File(__DIR__."/../../source/data/data.json");
|
||||
if (!isset($o->dh)) {
|
||||
$o->dh = $dh;
|
||||
} else {
|
||||
if ($o->dh != $dh) {
|
||||
$o->dh = $dh;
|
||||
$this->gatherData();
|
||||
$this->renderAndMove();
|
||||
$this->generateViewAllPages();
|
||||
$this->updateChangeTime();
|
||||
print "data/data.json changed...\n";
|
||||
};
|
||||
}
|
||||
|
||||
$c = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a given file into an md5 string
|
||||
* @param {String} file name to be hashed
|
||||
*
|
||||
* @return {String} md5 string of the file or an empty string if the file wasn't found
|
||||
*/
|
||||
private function md5File($f) {
|
||||
$r = file_exists($f) ? md5_file($f) : '';
|
||||
return $r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies a file from the given source path to the given public path
|
||||
* @param {String} the source pattern name
|
||||
* @param {String} the public pattern name
|
||||
*
|
||||
* @return {String} copied file
|
||||
*/
|
||||
private function moveFile($s,$p) {
|
||||
copy(__DIR__."/../../source".$s,__DIR__."/../../public".$p);
|
||||
}
|
||||
|
||||
}
|
23
config/config.ini.default
Normal file
23
config/config.ini.default
Normal file
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Configuration Options for Pattern Lab
|
||||
* If config.ini doesn't exist Pattern Lab will try to create a new version
|
||||
*/
|
||||
|
||||
// both the source and the public location of the patterns
|
||||
sp = "/../../source/patterns/";
|
||||
pp = "/../../public/patterns/";
|
||||
|
||||
// permissions for files created by PatternLab
|
||||
dp = 0775;
|
||||
fp = 0664;
|
||||
|
||||
// directories/files to ignore when iterating over the patterns directory. separate by commas
|
||||
if = ".,..,.DS_Store,d-wrapper";
|
||||
|
||||
// files to watch updates, second var is for where they go. separate by commas
|
||||
wf = "/scss/style.css"
|
||||
mf = "/css/style.css"
|
||||
|
||||
// choose which ports the websocket services should run on
|
||||
contentSyncPort = "8002"
|
||||
navSyncPort = "8003"
|
37
extras/apache/README
Normal file
37
extras/apache/README
Normal file
@ -0,0 +1,37 @@
|
||||
# How to Set-up Apache on Mac OS X
|
||||
|
||||
This document reviews how to (hopefully) easily set-up Apache on Mac OS X to support Pattern Lab. You'll need to open Terminal. Note that PHP may flake out with the default Apache install. I can modify directions in the future to account for that.
|
||||
|
||||
## 1. Modify hosts
|
||||
|
||||
First, you'll want to modify your hosts file so that you can use a specific hostname for the site rather than just `127.0.0.1` or `localhost`. To do so do the following:
|
||||
|
||||
1. In Terminal type `sudo vi /etc/hosts`
|
||||
2. When prompted, enter the password you use to log-in
|
||||
3. When the file loads type `i`
|
||||
4. Using the arrow keys get to the end of the last line
|
||||
5. Hit `return` and type `127.0.0.1 patternlab.localhost`
|
||||
6. Hit the `esc` key and type `:wq`
|
||||
|
||||
Your hosts should now be saved.
|
||||
|
||||
## 2. Modify Apache
|
||||
|
||||
Second, you'll want to add an Apache `VirtualHost` so that Apache will know to listen for your application at the correct hostname.
|
||||
|
||||
1. In Terminal type `sudo vi /etc/apache2/extra/httpd-vhosts.conf`
|
||||
2. When prompted, enter the password you use to log-in
|
||||
3. When the file loads type `i`
|
||||
4. Using the arrow keys get to the end of the last line
|
||||
5. Hit `return` twice
|
||||
6. Copy and paste the info from the `vhost.txt` file in this directory.
|
||||
7. Modify `DocumentRoot` path to match the location of the your install of Pattern Lab
|
||||
7. Hit the `esc` key and type `:wq`
|
||||
|
||||
## 3. Restart Apache
|
||||
|
||||
Last, you'll want to restart Apache so your changes take effect. Simply open System Preferences and go to the "Sharing" panel. Untick the "Web Sharing" checkbox and tick it again to restart Apache.
|
||||
|
||||
## 4. Test By Visiting patternlab.localhost
|
||||
|
||||
In a browser try to visit http://patternlab.localhost. You should get the Pattern Lab styleguide by default. If you get Google Search results just make sure you enter the http://
|
5
extras/apache/vhost.txt
Normal file
5
extras/apache/vhost.txt
Normal file
@ -0,0 +1,5 @@
|
||||
<VirtualHost *:80>
|
||||
DocumentRoot "/Users/dmolsen/Sites/patternlab/public"
|
||||
ServerName patternlab.localhost
|
||||
ServerAlias patternlab.*.xip.io
|
||||
</VirtualHost>
|
43
listeners/contentSyncBroadcasterServer.php
Normal file
43
listeners/contentSyncBroadcasterServer.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/*!
|
||||
* Content Sync Server, v0.1
|
||||
*
|
||||
* Copyright (c) 2013 Dave Olsen, http://dmolsen.com
|
||||
* Licensed under the MIT license
|
||||
*
|
||||
* The server that clients attach to to learn about content updates. See
|
||||
* lib/Wrench/Application/contentSyncBroadcasterApplication.php for logic
|
||||
*
|
||||
*/
|
||||
|
||||
// turn errors on or off for debugging purposes
|
||||
ini_set('display_errors', 0);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
require(__DIR__.'/lib/SplClassLoader.php');
|
||||
|
||||
// load wrench
|
||||
$classLoader = new SplClassLoader('Wrench',__DIR__.'/lib');
|
||||
$classLoader->register();
|
||||
|
||||
// parse the main config for the content sync port
|
||||
if (!($config = @parse_ini_file(__DIR__."/../config/config.ini"))) {
|
||||
print "Missing the configuration file. Please build it using the Pattern Lab builder.\n";
|
||||
exit;
|
||||
}
|
||||
|
||||
$port = ($config) ? trim($config['contentSyncPort']) : '8002';
|
||||
|
||||
// start the content sync server
|
||||
$server = new \Wrench\Server('ws://0.0.0.0:'.$port.'/', array());
|
||||
|
||||
// register the application
|
||||
$server->registerApplication('contentsync', new \Wrench\Application\contentSyncBroadcasterApplication());
|
||||
|
||||
print "\n";
|
||||
print "Auto-reload Server Started...\n";
|
||||
print "Use CTRL+C to stop this service...\n";
|
||||
|
||||
// run it
|
||||
$server->run();
|
13
listeners/lib/LICENSE
Executable file
13
listeners/lib/LICENSE
Executable file
@ -0,0 +1,13 @@
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
Version 2, December 2004
|
||||
|
||||
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim or modified
|
||||
copies of this license document, and changing it is allowed as long
|
||||
as the name is changed.
|
||||
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. You just DO WHAT THE FUCK YOU WANT TO.
|
136
listeners/lib/SplClassLoader.php
Executable file
136
listeners/lib/SplClassLoader.php
Executable file
@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* SplClassLoader implementation that implements the technical interoperability
|
||||
* standards for PHP 5.3 namespaces and class names.
|
||||
*
|
||||
* http://groups.google.com/group/php-standards/web/final-proposal
|
||||
*
|
||||
* // Example which loads classes for the Doctrine Common package in the
|
||||
* // Doctrine\Common namespace.
|
||||
* $classLoader = new SplClassLoader('Doctrine\Common', '/path/to/doctrine');
|
||||
* $classLoader->register();
|
||||
*
|
||||
* @author Jonathan H. Wage <jonwage@gmail.com>
|
||||
* @author Roman S. Borschel <roman@code-factory.org>
|
||||
* @author Matthew Weier O'Phinney <matthew@zend.com>
|
||||
* @author Kris Wallsmith <kris.wallsmith@gmail.com>
|
||||
* @author Fabien Potencier <fabien.potencier@symfony-project.org>
|
||||
*/
|
||||
class SplClassLoader
|
||||
{
|
||||
private $_fileExtension = '.php';
|
||||
private $_namespace;
|
||||
private $_includePath;
|
||||
private $_namespaceSeparator = '\\';
|
||||
|
||||
/**
|
||||
* Creates a new <tt>SplClassLoader</tt> that loads classes of the
|
||||
* specified namespace.
|
||||
*
|
||||
* @param string $ns The namespace to use.
|
||||
*/
|
||||
public function __construct($ns = null, $includePath = null)
|
||||
{
|
||||
$this->_namespace = $ns;
|
||||
$this->_includePath = $includePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the namespace separator used by classes in the namespace of this class loader.
|
||||
*
|
||||
* @param string $sep The separator to use.
|
||||
*/
|
||||
public function setNamespaceSeparator($sep)
|
||||
{
|
||||
$this->_namespaceSeparator = $sep;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the namespace seperator used by classes in the namespace of this class loader.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function getNamespaceSeparator()
|
||||
{
|
||||
return $this->_namespaceSeparator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the base include path for all class files in the namespace of this class loader.
|
||||
*
|
||||
* @param string $includePath
|
||||
*/
|
||||
public function setIncludePath($includePath)
|
||||
{
|
||||
$this->_includePath = $includePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the base include path for all class files in the namespace of this class loader.
|
||||
*
|
||||
* @return string $includePath
|
||||
*/
|
||||
public function getIncludePath()
|
||||
{
|
||||
return $this->_includePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the file extension of class files in the namespace of this class loader.
|
||||
*
|
||||
* @param string $fileExtension
|
||||
*/
|
||||
public function setFileExtension($fileExtension)
|
||||
{
|
||||
$this->_fileExtension = $fileExtension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the file extension of class files in the namespace of this class loader.
|
||||
*
|
||||
* @return string $fileExtension
|
||||
*/
|
||||
public function getFileExtension()
|
||||
{
|
||||
return $this->_fileExtension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs this class loader on the SPL autoload stack.
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
spl_autoload_register(array($this, 'loadClass'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstalls this class loader from the SPL autoloader stack.
|
||||
*/
|
||||
public function unregister()
|
||||
{
|
||||
spl_autoload_unregister(array($this, 'loadClass'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the given class or interface.
|
||||
*
|
||||
* @param string $className The name of the class to load.
|
||||
* @return void
|
||||
*/
|
||||
public function loadClass($className)
|
||||
{
|
||||
if (null === $this->_namespace || $this->_namespace.$this->_namespaceSeparator === substr($className, 0, strlen($this->_namespace.$this->_namespaceSeparator))) {
|
||||
$fileName = '';
|
||||
$namespace = '';
|
||||
if (false !== ($lastNsPos = strripos($className, $this->_namespaceSeparator))) {
|
||||
$namespace = substr($className, 0, $lastNsPos);
|
||||
$className = substr($className, $lastNsPos + 1);
|
||||
$fileName = str_replace($this->_namespaceSeparator, DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
$fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . $this->_fileExtension;
|
||||
|
||||
require ($this->_includePath !== null ? $this->_includePath . DIRECTORY_SEPARATOR : '') . $fileName;
|
||||
}
|
||||
}
|
||||
}
|
34
listeners/lib/Wrench/Application/Application.php
Executable file
34
listeners/lib/Wrench/Application/Application.php
Executable file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Application;
|
||||
|
||||
/**
|
||||
* Wrench Server Application
|
||||
*/
|
||||
abstract class Application
|
||||
{
|
||||
/**
|
||||
* Optional: handle a connection
|
||||
*/
|
||||
abstract public function onConnect($connection);
|
||||
|
||||
/**
|
||||
* Optional: handle a disconnection
|
||||
*
|
||||
* @param
|
||||
*/
|
||||
// abstract public function onDisconnect($connection);
|
||||
|
||||
/**
|
||||
* Optional: allow the application to perform any tasks which will result in a push to clients
|
||||
*/
|
||||
abstract public function onUpdate();
|
||||
|
||||
/**
|
||||
* Handle data received from a client
|
||||
*
|
||||
* @param Payload $payload A payload object, that supports __toString()
|
||||
* @param Connection $connection
|
||||
*/
|
||||
//abstract public function onData($payload, $connection);
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
/*!
|
||||
* Content Sync Broadcaster, v0.1
|
||||
*
|
||||
* Copyright (c) 2013 Dave Olsen, http://dmolsen.com
|
||||
* Licensed under the MIT license
|
||||
*
|
||||
* Continuously pushes data from latest-change.txt to attached browsers.
|
||||
* latest-change.txt is modified by the watch feature of Pattern Lab. Attached
|
||||
* browsers should refresh when they see the data change.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Wrench\Application;
|
||||
|
||||
use Wrench\Application\Application;
|
||||
use Wrench\Application\NamedApplication;
|
||||
|
||||
class contentSyncBroadcasterApplication extends Application {
|
||||
|
||||
protected $clients = array();
|
||||
protected $savedTimestamp = null;
|
||||
protected $c = false;
|
||||
|
||||
/**
|
||||
* Set the saved timestamp. If the latest-change file doesn't exist simply use the current time as the saved time
|
||||
*/
|
||||
public function __construct() {
|
||||
|
||||
if (file_exists(__DIR__."/../../../../public/latest-change.txt")) {
|
||||
$this->savedTimestamp = file_get_contents(__DIR__."/../../../../public/latest-change.txt");
|
||||
} else {
|
||||
$this->savedTimestamp = time();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* When a client connects add it to the list of connected clients
|
||||
*/
|
||||
public function onConnect($client) {
|
||||
$id = $client->getId();
|
||||
$this->clients[$id] = $client;
|
||||
}
|
||||
|
||||
/**
|
||||
* When a client disconnects remove it from the list of connected clients
|
||||
*/
|
||||
public function onDisconnect($client) {
|
||||
$id = $client->getId();
|
||||
unset($this->clients[$id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dead function in this instance
|
||||
*/
|
||||
public function onData($data, $client) {
|
||||
// function not in use
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends out a message once a second to all connected clients containing the contents of latest-change.txt
|
||||
*/
|
||||
public function onUpdate() {
|
||||
|
||||
if (file_exists(__DIR__."/../../../../public/latest-change.txt")) {
|
||||
$readTimestamp = file_get_contents(__DIR__."/../../../../public/latest-change.txt");
|
||||
if ($readTimestamp != $this->savedTimestamp) {
|
||||
foreach ($this->clients as $sendto) {
|
||||
$sendto->send($readTimestamp);
|
||||
}
|
||||
$this->savedTimestamp = $readTimestamp;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
/*!
|
||||
* Nav Sync Broadcaster, v0.1
|
||||
*
|
||||
* Copyright (c) 2013 Dave Olsen, http://dmolsen.com
|
||||
* Licensed under the MIT license
|
||||
*
|
||||
* Listens for when a page changes in one of the attached browsers. Sends the
|
||||
* change to all listening browsers so they can also update.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Wrench\Application;
|
||||
|
||||
use Wrench\Application\Application;
|
||||
use Wrench\Application\NamedApplication;
|
||||
|
||||
class navSyncBroadcasterApplication extends Application {
|
||||
|
||||
protected $clients = array();
|
||||
protected $currentAddress = null;
|
||||
|
||||
/**
|
||||
* When a client connects add it to the list of connected clients. Also send the client the current page to load in their iframe
|
||||
*/
|
||||
public function onConnect($client) {
|
||||
$id = $client->getId();
|
||||
$this->clients[$id] = $client;
|
||||
if ($this->currentAddress != null) {
|
||||
$client->send($this->currentAddress);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When a client disconnects remove it from the list of connected clients
|
||||
*/
|
||||
public function onDisconnect($client) {
|
||||
$id = $client->getId();
|
||||
unset($this->clients[$id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* When receiving a message from a client, strip it to avoid cross-domain issues and send it to all clients except the one who sent it
|
||||
* Also store the address as the current address for any new clients that attach
|
||||
*/
|
||||
public function onData($data, $client) {
|
||||
|
||||
preg_match("/http:\/\/[A-z0-9\-\.]{1,}\/(.*)/i",$data,$matches);
|
||||
$data = "/".$matches[1];
|
||||
$testId = $client->getId();
|
||||
foreach ($this->clients as $sendto) {
|
||||
if ($testId != $sendto->getId()) {
|
||||
$sendto->send($data);
|
||||
}
|
||||
}
|
||||
$this->currentAddress = $data;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Dead function in this instance
|
||||
*/
|
||||
public function onUpdate() {
|
||||
// not using for this application
|
||||
}
|
||||
|
||||
}
|
70
listeners/lib/Wrench/BasicServer.php
Executable file
70
listeners/lib/Wrench/BasicServer.php
Executable file
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench;
|
||||
|
||||
use Wrench\Server;
|
||||
|
||||
class BasicServer extends Server
|
||||
{
|
||||
protected $rateLimiter;
|
||||
protected $originPolicy;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $uri
|
||||
* @param array $options
|
||||
*/
|
||||
public function __construct($uri, array $options = array())
|
||||
{
|
||||
parent::__construct($uri, $options);
|
||||
|
||||
$this->configureRateLimiter();
|
||||
$this->configureOriginPolicy();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Wrench.Server::configure()
|
||||
*/
|
||||
protected function configure(array $options)
|
||||
{
|
||||
$options = array_merge(array(
|
||||
'check_origin' => true,
|
||||
'allowed_origins' => array(),
|
||||
'origin_policy_class' => 'Wrench\Listener\OriginPolicy',
|
||||
'rate_limiter_class' => 'Wrench\Listener\RateLimiter'
|
||||
), $options);
|
||||
|
||||
parent::configure($options);
|
||||
}
|
||||
|
||||
protected function configureRateLimiter()
|
||||
{
|
||||
$class = $this->options['rate_limiter_class'];
|
||||
$this->rateLimiter = new $class();
|
||||
$this->rateLimiter->listen($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the origin policy
|
||||
*/
|
||||
protected function configureOriginPolicy()
|
||||
{
|
||||
$class = $this->options['origin_policy_class'];
|
||||
$this->originPolicy = new $class($this->options['allowed_origins']);
|
||||
|
||||
if ($this->options['check_origin']) {
|
||||
$this->originPolicy->listen($this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an allowed origin
|
||||
*
|
||||
* @param array $origin
|
||||
*/
|
||||
public function addAllowedOrigin($origin)
|
||||
{
|
||||
$this->originPolicy->addAllowedOrigin($origin);
|
||||
}
|
||||
}
|
265
listeners/lib/Wrench/Client.php
Executable file
265
listeners/lib/Wrench/Client.php
Executable file
@ -0,0 +1,265 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench;
|
||||
|
||||
use Wrench\Payload\Payload;
|
||||
|
||||
use Wrench\Payload\PayloadHandler;
|
||||
|
||||
use Wrench\Util\Configurable;
|
||||
|
||||
use Wrench\Socket\ClientSocket;
|
||||
use Wrench\Protocol\Protocol;
|
||||
use Wrench\Protocol\Rfc6455Protocol;
|
||||
|
||||
use \InvalidArgumentException;
|
||||
use \RuntimeException;
|
||||
|
||||
/**
|
||||
* Client class
|
||||
*
|
||||
* Represents a Wrench client
|
||||
*/
|
||||
class Client extends Configurable
|
||||
{
|
||||
/**
|
||||
* @var int bytes
|
||||
*/
|
||||
const MAX_HANDSHAKE_RESPONSE = '1500';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $uri;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $origin;
|
||||
|
||||
/**
|
||||
* @var ClientSocket
|
||||
*/
|
||||
protected $socket;
|
||||
|
||||
/**
|
||||
* Request headers
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $headers = array();
|
||||
|
||||
/**
|
||||
* Whether the client is connected
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $connected = false;
|
||||
|
||||
/**
|
||||
* @var PayloadHandler
|
||||
*/
|
||||
protected $payloadHandler = null;
|
||||
|
||||
/**
|
||||
* Complete received payloads
|
||||
*
|
||||
* @var array<Payload>
|
||||
*/
|
||||
protected $received = array();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $uri
|
||||
* @param string $origin The origin to include in the handshake (required
|
||||
* in later versions of the protocol)
|
||||
* @param array $options (optional) Array of options
|
||||
* - socket => Socket instance (otherwise created)
|
||||
* - protocol => Protocol
|
||||
*/
|
||||
public function __construct($uri, $origin, array $options = array())
|
||||
{
|
||||
parent::__construct($options);
|
||||
|
||||
$uri = (string)$uri;
|
||||
if (!$uri) {
|
||||
throw new InvalidArgumentException('No URI specified');
|
||||
}
|
||||
$this->uri = $uri;
|
||||
|
||||
$origin = (string)$origin;
|
||||
if (!$origin) {
|
||||
throw new InvalidArgumentException('No origin specified');
|
||||
}
|
||||
$this->origin = $origin;
|
||||
|
||||
$this->protocol->validateUri($this->uri);
|
||||
$this->protocol->validateOriginUri($this->origin);
|
||||
|
||||
$this->configureSocket();
|
||||
$this->configurePayloadHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure options
|
||||
*
|
||||
* @param array $options
|
||||
* @return void
|
||||
*/
|
||||
protected function configure(array $options)
|
||||
{
|
||||
$options = array_merge(array(
|
||||
'socket_class' => 'Wrench\\Socket\\ClientSocket',
|
||||
'on_data_callback' => null
|
||||
), $options);
|
||||
|
||||
parent::configure($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the client socket
|
||||
*/
|
||||
protected function configureSocket()
|
||||
{
|
||||
$class = $this->options['socket_class'];
|
||||
$this->socket = new $class($this->uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the payload handler
|
||||
*/
|
||||
protected function configurePayloadHandler()
|
||||
{
|
||||
$this->payloadHandler = new PayloadHandler(array($this, 'onData'), $this->options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload receiver
|
||||
*
|
||||
* Public because called from our PayloadHandler. Don't call us, we'll call
|
||||
* you (via the on_data_callback option).
|
||||
*
|
||||
* @param Payload $payload
|
||||
*/
|
||||
public function onData(Payload $payload)
|
||||
{
|
||||
$this->received[] = $payload;
|
||||
if (($callback = $this->options['on_data_callback'])) {
|
||||
call_user_func($callback, $payload);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a request header to be included in the initial handshake
|
||||
*
|
||||
* For example, to include a Cookie header
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $value
|
||||
* @return void
|
||||
*/
|
||||
public function addRequestHeader($name, $value)
|
||||
{
|
||||
$this->headers[$name] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends data to the socket
|
||||
*
|
||||
* @param string $data
|
||||
* @param string $type Payload type
|
||||
* @param boolean $masked
|
||||
* @return boolean Success
|
||||
*/
|
||||
public function sendData($data, $type = Protocol::TYPE_TEXT, $masked = true)
|
||||
{
|
||||
if (is_string($type) && isset(Protocol::$frameTypes[$type])) {
|
||||
$type = Protocol::$frameTypes[$type];
|
||||
}
|
||||
|
||||
$payload = $this->protocol->getPayload();
|
||||
|
||||
$payload->encode(
|
||||
$data,
|
||||
$type,
|
||||
$masked
|
||||
);
|
||||
|
||||
return $payload->sendToSocket($this->socket);
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives data sent by the server
|
||||
*
|
||||
* @param callable $callback
|
||||
* @return array<Payload> Payload received since the last call to receive()
|
||||
*/
|
||||
public function receive()
|
||||
{
|
||||
if (!$this->isConnected()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = $this->socket->receive();
|
||||
|
||||
if (!$data) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$old = $this->received;
|
||||
$this->payloadHandler->handle($data);
|
||||
return array_diff($this->received, $old);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the Wrench server
|
||||
*
|
||||
* @return boolean Whether a new connection was made
|
||||
*/
|
||||
public function connect()
|
||||
{
|
||||
if ($this->isConnected()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->socket->connect();
|
||||
|
||||
$key = $this->protocol->generateKey();
|
||||
$handshake = $this->protocol->getRequestHandshake(
|
||||
$this->uri,
|
||||
$key,
|
||||
$this->origin,
|
||||
$this->headers
|
||||
);
|
||||
|
||||
$this->socket->send($handshake);
|
||||
$response = $this->socket->receive(self::MAX_HANDSHAKE_RESPONSE);
|
||||
return ($this->connected =
|
||||
$this->protocol->validateResponseHandshake($response, $key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the client is currently connected
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isConnected()
|
||||
{
|
||||
return $this->connected;
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Bug: what if connect has been called twice. The first socket never
|
||||
* gets closed.
|
||||
*/
|
||||
public function disconnect()
|
||||
{
|
||||
if ($this->socket) {
|
||||
$this->socket->disconnect();
|
||||
}
|
||||
$this->connected = false;
|
||||
}
|
||||
|
||||
|
||||
}
|
500
listeners/lib/Wrench/Connection.php
Executable file
500
listeners/lib/Wrench/Connection.php
Executable file
@ -0,0 +1,500 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench;
|
||||
|
||||
use Wrench\Payload\PayloadHandler;
|
||||
|
||||
use Wrench\Protocol\Protocol;
|
||||
|
||||
use Wrench\Payload\Payload;
|
||||
|
||||
use Wrench\Util\Configurable;
|
||||
use Wrench\Socket\ServerClientSocket;
|
||||
use Wrench\Server;
|
||||
use Wrench\Exception as WrenchException;
|
||||
use Wrench\Exception\CloseException;
|
||||
use Wrench\Exception\ConnectionException;
|
||||
use Wrench\Exception\HandshakeException;
|
||||
use Wrench\Exception\BadRequestException;
|
||||
|
||||
use \Exception;
|
||||
use \RuntimeException;
|
||||
|
||||
/**
|
||||
* Represents a client connection on the server side
|
||||
*
|
||||
* i.e. the `Server` manages a bunch of `Connection`s
|
||||
*/
|
||||
class Connection extends Configurable
|
||||
{
|
||||
/**
|
||||
* The connection manager
|
||||
*
|
||||
* @var Wrench\ConnectionManager
|
||||
*/
|
||||
protected $manager;
|
||||
|
||||
/**
|
||||
* Socket object
|
||||
*
|
||||
* Wraps the client connection resource
|
||||
*
|
||||
* @var ServerClientSocket
|
||||
*/
|
||||
protected $socket;
|
||||
|
||||
/**
|
||||
* Whether the connection has successfully handshaken
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $handshaked = false;
|
||||
|
||||
/**
|
||||
* The application this connection belongs to
|
||||
*
|
||||
* @var Application
|
||||
*/
|
||||
protected $application = null;
|
||||
|
||||
/**
|
||||
* The IP address of the client
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $ip;
|
||||
|
||||
/**
|
||||
* The port of the client
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $port;
|
||||
|
||||
/**
|
||||
* Connection ID
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $id = null;
|
||||
|
||||
/**
|
||||
* @var PayloadHandler
|
||||
*/
|
||||
protected $payloadHandler;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param Server $server
|
||||
* @param ServerClientSocket $socket
|
||||
* @param array $options
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __construct(
|
||||
ConnectionManager $manager,
|
||||
ServerClientSocket $socket,
|
||||
array $options = array()
|
||||
) {
|
||||
$this->manager = $manager;
|
||||
$this->socket = $socket;
|
||||
|
||||
|
||||
parent::__construct($options);
|
||||
|
||||
$this->configureClientInformation();
|
||||
$this->configurePayloadHandler();
|
||||
|
||||
$this->log('Connected');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the connection manager of this connection
|
||||
*
|
||||
* @return \Wrench\ConnectionManager
|
||||
*/
|
||||
public function getConnectionManager()
|
||||
{
|
||||
return $this->manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Wrench\Util.Configurable::configure()
|
||||
*/
|
||||
protected function configure(array $options)
|
||||
{
|
||||
$options = array_merge(array(
|
||||
'connection_id_secret' => 'asu5gj656h64Da(0crt8pud%^WAYWW$u76dwb',
|
||||
'connection_id_algo' => 'sha512',
|
||||
), $options);
|
||||
|
||||
parent::configure($options);
|
||||
}
|
||||
|
||||
protected function configurePayloadHandler()
|
||||
{
|
||||
$this->payloadHandler = new PayloadHandler(
|
||||
array($this, 'handlePayload'),
|
||||
$this->options
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
protected function configureClientInformation()
|
||||
{
|
||||
$this->ip = $this->socket->getIp();
|
||||
$this->port = $this->socket->getPort();
|
||||
$this->configureClientId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the client ID
|
||||
*
|
||||
* We hash the client ID to prevent leakage of information if another client
|
||||
* happens to get a hold of an ID. The secret *must* be lengthy, and must
|
||||
* be kept secret for this to work: otherwise it's trivial to search the space
|
||||
* of possible IP addresses/ports (well, if not trivial, at least very fast).
|
||||
*/
|
||||
protected function configureClientId()
|
||||
{
|
||||
$message = sprintf(
|
||||
'%s:uri=%s&ip=%s&port=%s',
|
||||
$this->options['connection_id_secret'],
|
||||
rawurlencode($this->manager->getUri()),
|
||||
rawurlencode($this->ip),
|
||||
rawurlencode($this->port)
|
||||
);
|
||||
|
||||
$algo = $this->options['connection_id_algo'];
|
||||
|
||||
if (extension_loaded('gmp')) {
|
||||
$hash = hash($algo, $message, true);
|
||||
$hash = gmp_strval(gmp_init($hash, 16), 62);
|
||||
} else {
|
||||
// @codeCoverageIgnoreStart
|
||||
$hash = hash($algo, $message);
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
$this->id = $hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data receiver
|
||||
*
|
||||
* Called by the connection manager when the connection has received data
|
||||
*
|
||||
* @param string $data
|
||||
*/
|
||||
public function onData($data)
|
||||
{
|
||||
if (!$this->handshaked) {
|
||||
return $this->handshake($data);
|
||||
}
|
||||
return $this->handle($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a websocket handshake
|
||||
*
|
||||
* @param string $data
|
||||
* @throws BadRequestException
|
||||
* @throws HandshakeException
|
||||
* @throws WrenchException
|
||||
*/
|
||||
public function handshake($data)
|
||||
{
|
||||
try {
|
||||
list($path, $origin, $key, $extensions)
|
||||
= $this->protocol->validateRequestHandshake($data);
|
||||
|
||||
$this->application = $this->manager->getApplicationForPath($path);
|
||||
|
||||
if (!$this->application) {
|
||||
throw new BadRequestException('Invalid application');
|
||||
}
|
||||
|
||||
$this->manager->getServer()->notify(
|
||||
Server::EVENT_HANDSHAKE_REQUEST,
|
||||
array($this, $path, $origin, $key, $extensions)
|
||||
);
|
||||
|
||||
$response = $this->protocol->getResponseHandshake($key);
|
||||
|
||||
if (!$this->socket->isConnected()) {
|
||||
throw new HandshakeException('Socket is not connected');
|
||||
}
|
||||
|
||||
if ($this->socket->send($response) === false) {
|
||||
throw new HandshakeException('Could not send handshake response');
|
||||
}
|
||||
|
||||
$this->handshaked = true;
|
||||
|
||||
$this->log(sprintf(
|
||||
'Handshake successful: %s:%d (%s) connected to %s',
|
||||
$this->getIp(),
|
||||
$this->getPort(),
|
||||
$this->getId(),
|
||||
$path
|
||||
), 'info');
|
||||
|
||||
$this->manager->getServer()->notify(
|
||||
Server::EVENT_HANDSHAKE_SUCCESSFUL,
|
||||
array($this)
|
||||
);
|
||||
|
||||
if (method_exists($this->application, 'onConnect')) {
|
||||
$this->application->onConnect($this);
|
||||
}
|
||||
} catch (WrenchException $e) {
|
||||
$this->log('Handshake failed: ' . $e, 'err');
|
||||
$this->close($e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string export of the given binary data
|
||||
*
|
||||
* @param string $data
|
||||
* @return string
|
||||
*/
|
||||
protected function export($data)
|
||||
{
|
||||
$export = '';
|
||||
foreach (str_split($data) as $chr) {
|
||||
$export .= '\\x' . ord($chr);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle data received from the client
|
||||
*
|
||||
* The data passed in may belong to several different frames across one or
|
||||
* more protocols. It may not even contain a single complete frame. This method
|
||||
* manages slotting the data into separate payload objects.
|
||||
*
|
||||
* @todo An endpoint MUST be capable of handling control frames in the
|
||||
* middle of a fragmented message.
|
||||
* @param string $data
|
||||
* @return void
|
||||
*/
|
||||
public function handle($data)
|
||||
{
|
||||
$this->payloadHandler->handle($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a complete payload received from the client
|
||||
*
|
||||
* Public because called from our PayloadHandler
|
||||
*
|
||||
* @param string $payload
|
||||
*/
|
||||
public function handlePayload(Payload $payload)
|
||||
{
|
||||
$app = $this->getClientApplication();
|
||||
|
||||
$this->log('Handling payload: ' . $payload->getPayload(), 'debug');
|
||||
|
||||
switch ($type = $payload->getType()) {
|
||||
case Protocol::TYPE_TEXT:
|
||||
if (method_exists($app, 'onData')) {
|
||||
$app->onData($payload, $this);
|
||||
}
|
||||
return;
|
||||
|
||||
case Protocol::TYPE_BINARY:
|
||||
if(method_exists($app, 'onBinaryData')) {
|
||||
$app->onBinaryData($payload, $this);
|
||||
} else {
|
||||
$this->close(1003);
|
||||
}
|
||||
break;
|
||||
|
||||
case Protocol::TYPE_PING:
|
||||
$this->log('Ping received', 'notice');
|
||||
$this->send($payload->getPayload(), Protocol::TYPE_PONG);
|
||||
$this->log('Pong!', 'debug');
|
||||
break;
|
||||
|
||||
/**
|
||||
* A Pong frame MAY be sent unsolicited. This serves as a
|
||||
* unidirectional heartbeat. A response to an unsolicited Pong
|
||||
* frame is not expected.
|
||||
*/
|
||||
case Protocol::TYPE_PONG:
|
||||
$this->log('Received unsolicited pong', 'info');
|
||||
break;
|
||||
|
||||
case Protocol::TYPE_CLOSE:
|
||||
$this->log('Close frame received', 'notice');
|
||||
$this->close();
|
||||
$this->log('Disconnected', 'info');
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ConnectionException('Unhandled payload type');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the payload to the connection
|
||||
*
|
||||
* @param string $payload
|
||||
* @param string $type
|
||||
* @throws HandshakeException
|
||||
* @throws ConnectionException
|
||||
* @return boolean
|
||||
*/
|
||||
public function send($data, $type = Protocol::TYPE_TEXT)
|
||||
{
|
||||
if (!$this->handshaked) {
|
||||
throw new HandshakeException('Connection is not handshaked');
|
||||
}
|
||||
|
||||
$payload = $this->protocol->getPayload();
|
||||
|
||||
// Servers don't send masked payloads
|
||||
$payload->encode($data, $type, false);
|
||||
|
||||
if (!$payload->sendToSocket($this->socket)) {
|
||||
$this->log('Could not send payload to client', 'warn');
|
||||
throw new ConnectionException('Could not send data to connection: ' . $this->socket->getLastError());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes data on the socket
|
||||
*
|
||||
* @throws CloseException
|
||||
*/
|
||||
public function process()
|
||||
{
|
||||
$data = $this->socket->receive();
|
||||
$bytes = strlen($data);
|
||||
|
||||
if ($bytes === 0 || $data === false) {
|
||||
throw new CloseException('Error reading data from socket: ' . $this->socket->getLastError());
|
||||
}
|
||||
|
||||
$this->onData($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the connection according to the WebSocket protocol
|
||||
*
|
||||
* If an endpoint receives a Close frame and that endpoint did not
|
||||
* previously send a Close frame, the endpoint MUST send a Close frame
|
||||
* in response. It SHOULD do so as soon as is practical. An endpoint
|
||||
* MAY delay sending a close frame until its current message is sent
|
||||
* (for instance, if the majority of a fragmented message is already
|
||||
* sent, an endpoint MAY send the remaining fragments before sending a
|
||||
* Close frame). However, there is no guarantee that the endpoint which
|
||||
* has already sent a Close frame will continue to process data.
|
||||
|
||||
* After both sending and receiving a close message, an endpoint
|
||||
* considers the WebSocket connection closed, and MUST close the
|
||||
* underlying TCP connection. The server MUST close the underlying TCP
|
||||
* connection immediately; the client SHOULD wait for the server to
|
||||
* close the connection but MAY close the connection at any time after
|
||||
* sending and receiving a close message, e.g. if it has not received a
|
||||
* TCP close from the server in a reasonable time period.
|
||||
*
|
||||
* @param int|Exception $statusCode
|
||||
* @return boolean
|
||||
*/
|
||||
public function close($code = Protocol::CLOSE_NORMAL)
|
||||
{
|
||||
try {
|
||||
if (!$this->handshaked) {
|
||||
$response = $this->protocol->getResponseError($code);
|
||||
$this->socket->send($response);
|
||||
} else {
|
||||
$response = $this->protocol->getCloseFrame($code);
|
||||
$this->socket->send($response);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->log('Unable to send close message', 'warning');
|
||||
}
|
||||
|
||||
if ($this->application && method_exists($this->application, 'onDisconnect')) {
|
||||
$this->application->onDisconnect($this);
|
||||
}
|
||||
|
||||
$this->socket->disconnect();
|
||||
$this->manager->removeConnection($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message
|
||||
*
|
||||
* @param string $message
|
||||
* @param string $priority
|
||||
*/
|
||||
public function log($message, $priority = 'info')
|
||||
{
|
||||
$this->manager->log(sprintf(
|
||||
'%s: %s:%d (%s): %s',
|
||||
__CLASS__,
|
||||
$this->getIp(),
|
||||
$this->getPort(),
|
||||
$this->getId(),
|
||||
$message
|
||||
), $priority);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the IP address of the connection
|
||||
*
|
||||
* @return string Usually dotted quad notation
|
||||
*/
|
||||
public function getIp()
|
||||
{
|
||||
return $this->ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the port of the connection
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getPort()
|
||||
{
|
||||
return $this->port;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the connection ID
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the socket object
|
||||
*
|
||||
* @return Socket\ServerClientSocket
|
||||
*/
|
||||
public function getSocket()
|
||||
{
|
||||
return $this->socket;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the client application
|
||||
*
|
||||
* @return Application
|
||||
*/
|
||||
public function getClientApplication()
|
||||
{
|
||||
return (isset($this->application)) ? $this->application : false;
|
||||
}
|
||||
}
|
332
listeners/lib/Wrench/ConnectionManager.php
Executable file
332
listeners/lib/Wrench/ConnectionManager.php
Executable file
@ -0,0 +1,332 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench;
|
||||
|
||||
use Wrench\Protocol\Protocol;
|
||||
use Wrench\Resource;
|
||||
use Wrench\Util\Configurable;
|
||||
use Wrench\Exception\Exception as WrenchException;
|
||||
use Wrench\Exception\CloseException;
|
||||
use \Exception;
|
||||
use \Countable;
|
||||
|
||||
class ConnectionManager extends Configurable implements Countable
|
||||
{
|
||||
const TIMEOUT_SELECT = 0;
|
||||
const TIMEOUT_SELECT_MICROSEC = 200000;
|
||||
|
||||
/**
|
||||
* @var Server
|
||||
*/
|
||||
protected $server;
|
||||
|
||||
/**
|
||||
* Master socket
|
||||
*
|
||||
* @var Socket
|
||||
*/
|
||||
protected $socket;
|
||||
|
||||
/**
|
||||
* An array of client connections
|
||||
*
|
||||
* @var array<int => Connection>
|
||||
*/
|
||||
protected $connections = array();
|
||||
|
||||
/**
|
||||
* An array of raw socket resources, corresponding to connections, roughly
|
||||
*
|
||||
* @var array<int => resource>
|
||||
*/
|
||||
protected $resources = array();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param Server $server
|
||||
* @param array $options
|
||||
*/
|
||||
public function __construct(Server $server, array $options = array())
|
||||
{
|
||||
$this->server = $server;
|
||||
|
||||
parent::__construct($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Countable::count()
|
||||
*/
|
||||
public function count()
|
||||
{
|
||||
return count($this->connections);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Wrench\Socket.Socket::configure()
|
||||
* Options include:
|
||||
* - timeout_select => int, seconds, default 0
|
||||
* - timeout_select_microsec => int, microseconds (NB: not milli), default: 200000
|
||||
*/
|
||||
protected function configure(array $options)
|
||||
{
|
||||
$options = array_merge(array(
|
||||
'socket_master_class' => 'Wrench\Socket\ServerSocket',
|
||||
'socket_master_options' => array(),
|
||||
'socket_client_class' => 'Wrench\Socket\ServerClientSocket',
|
||||
'socket_client_options' => array(),
|
||||
'connection_class' => 'Wrench\Connection',
|
||||
'connection_options' => array(),
|
||||
'timeout_select' => self::TIMEOUT_SELECT,
|
||||
'timeout_select_microsec' => self::TIMEOUT_SELECT_MICROSEC
|
||||
), $options);
|
||||
|
||||
parent::configure($options);
|
||||
|
||||
$this->configureMasterSocket();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the application associated with the given path
|
||||
*
|
||||
* @param string $path
|
||||
*/
|
||||
public function getApplicationForPath($path)
|
||||
{
|
||||
$path = ltrim($path, '/');
|
||||
return $this->server->getApplication($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the main server socket
|
||||
*
|
||||
* @param string $uri
|
||||
*/
|
||||
protected function configureMasterSocket()
|
||||
{
|
||||
$class = $this->options['socket_master_class'];
|
||||
$options = $this->options['socket_master_options'];
|
||||
$this->socket = new $class($this->server->getUri(), $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens on the main socket
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function listen()
|
||||
{
|
||||
$this->socket->listen();
|
||||
$this->resources[$this->socket->getResourceId()] = $this->socket->getResource();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all resources
|
||||
*
|
||||
* @return array<int => resource)
|
||||
*/
|
||||
protected function getAllResources()
|
||||
{
|
||||
return array_merge($this->resources, array(
|
||||
$this->socket->getResourceId() => $this->socket->getResource()
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Connection associated with the specified socket resource
|
||||
*
|
||||
* @param resource $socket
|
||||
* @return Connection
|
||||
*/
|
||||
protected function getConnectionForClientSocket($socket)
|
||||
{
|
||||
if (!isset($this->connections[$this->resourceId($socket)])) {
|
||||
return false;
|
||||
}
|
||||
return $this->connections[$this->resourceId($socket)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Select and process an array of resources
|
||||
*
|
||||
* @param array $resources
|
||||
*/
|
||||
public function selectAndProcess()
|
||||
{
|
||||
$read = $this->resources;
|
||||
$unused_write = null;
|
||||
$unsued_exception = null;
|
||||
|
||||
stream_select(
|
||||
$read,
|
||||
$unused_write,
|
||||
$unused_exception,
|
||||
$this->options['timeout_select'],
|
||||
$this->options['timeout_select_microsec']
|
||||
);
|
||||
|
||||
foreach ($read as $socket) {
|
||||
if ($socket == $this->socket->getResource()) {
|
||||
$this->processMasterSocket();
|
||||
} else {
|
||||
$this->processClientSocket($socket);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process events on the master socket ($this->socket)
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function processMasterSocket()
|
||||
{
|
||||
$new = null;
|
||||
|
||||
try {
|
||||
$new = $this->socket->accept();
|
||||
} catch (Exception $e) {
|
||||
$this->server->log('Socket error: ' . $e, 'err');
|
||||
return;
|
||||
}
|
||||
|
||||
$connection = $this->createConnection($new);
|
||||
$this->server->notify(Server::EVENT_SOCKET_CONNECT, array($new, $connection));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a connection from a socket resource
|
||||
*
|
||||
* The create connection object is based on the options passed into the
|
||||
* constructor ('connection_class', 'connection_options'). This connection
|
||||
* instance and its associated socket resource are then stored in the
|
||||
* manager.
|
||||
*
|
||||
* @param resource $resource A socket resource
|
||||
* @return Connection
|
||||
*/
|
||||
protected function createConnection($resource)
|
||||
{
|
||||
if (!$resource || !is_resource($resource)) {
|
||||
throw new InvalidArgumentException('Invalid connection resource');
|
||||
}
|
||||
|
||||
$socket_class = $this->options['socket_client_class'];
|
||||
$socket_options = $this->options['socket_client_options'];
|
||||
|
||||
$connection_class = $this->options['connection_class'];
|
||||
$connection_options = $this->options['connection_options'];
|
||||
|
||||
$socket = new $socket_class($resource, $socket_options);
|
||||
$connection = new $connection_class($this, $socket, $connection_options);
|
||||
|
||||
$id = $this->resourceId($resource);
|
||||
$this->resources[$id] = $resource;
|
||||
$this->connections[$id] = $connection;
|
||||
|
||||
return $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process events on a client socket
|
||||
*
|
||||
* @param resource $socket
|
||||
*/
|
||||
protected function processClientSocket($socket)
|
||||
{
|
||||
$connection = $this->getConnectionForClientSocket($socket);
|
||||
|
||||
if (!$connection) {
|
||||
$this->log('No connection for client socket', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$connection->process();
|
||||
} catch (CloseException $e) {
|
||||
$this->log('Client connection closed: ' . $e, 'notice');
|
||||
$connection->close($e);
|
||||
} catch (WrenchException $e) {
|
||||
$this->log('Error on client socket: ' . $e, 'warning');
|
||||
$connection->close($e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This server makes an explicit assumption: PHP resource types may be cast
|
||||
* to a integer. Furthermore, we assume this is bijective. Both seem to be
|
||||
* true in most circumstances, but may not be guaranteed.
|
||||
*
|
||||
* This method (and $this->getResourceId()) exist to make this assumption
|
||||
* explicit.
|
||||
*
|
||||
* This is needed on the connection manager as well as on resources
|
||||
*
|
||||
* @param resource $resource
|
||||
*/
|
||||
protected function resourceId($resource)
|
||||
{
|
||||
return (int)$resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the connection manager's listening URI
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUri()
|
||||
{
|
||||
return $this->server->getUri();
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message
|
||||
*
|
||||
* @param string $message
|
||||
* @param string $priority
|
||||
*/
|
||||
public function log($message, $priority = 'info')
|
||||
{
|
||||
$this->server->log(sprintf(
|
||||
'%s: %s',
|
||||
__CLASS__,
|
||||
$message
|
||||
), $priority);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Wrench\Server
|
||||
*/
|
||||
public function getServer()
|
||||
{
|
||||
return $this->server;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a connection
|
||||
*
|
||||
* @param Connection $connection
|
||||
*/
|
||||
public function removeConnection(Connection $connection)
|
||||
{
|
||||
$socket = $connection->getSocket();
|
||||
|
||||
if ($socket->getResource()) {
|
||||
$index = $socket->getResourceId();
|
||||
} else {
|
||||
$index = array_search($connection, $this->connections);
|
||||
}
|
||||
|
||||
if (!$index) {
|
||||
$this->log('Could not remove connection: not found', 'warning');
|
||||
}
|
||||
|
||||
unset($this->connections[$index]);
|
||||
unset($this->resources[$index]);
|
||||
|
||||
$this->server->notify(
|
||||
Server::EVENT_SOCKET_DISCONNECT,
|
||||
array($connection->getSocket(), $connection)
|
||||
);
|
||||
}
|
||||
}
|
22
listeners/lib/Wrench/Exception/BadRequestException.php
Executable file
22
listeners/lib/Wrench/Exception/BadRequestException.php
Executable file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Exception;
|
||||
|
||||
use Wrench\Protocol\Protocol;
|
||||
use Wrench\Exception\HandshakeException;
|
||||
|
||||
class BadRequestException extends HandshakeException
|
||||
{
|
||||
/**
|
||||
* @param string $message
|
||||
* @param int $code
|
||||
* @param Exception $previous
|
||||
*/
|
||||
public function __construct($message = null, $code = null, $previous = null)
|
||||
{
|
||||
if ($code == null) {
|
||||
$code = Protocol::HTTP_BAD_REQUEST;
|
||||
}
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
25
listeners/lib/Wrench/Exception/CloseException.php
Executable file
25
listeners/lib/Wrench/Exception/CloseException.php
Executable file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Exception;
|
||||
|
||||
use Wrench\Protocol\Protocol;
|
||||
use Wrench\Exception\Exception as WrenchException;
|
||||
|
||||
/**
|
||||
* Close connection exception
|
||||
*/
|
||||
class CloseException extends WrenchException
|
||||
{
|
||||
/**
|
||||
* @param string $message
|
||||
* @param int $code
|
||||
* @param Exception $previous
|
||||
*/
|
||||
public function __construct($message = null, $code = null, $previous = null)
|
||||
{
|
||||
if ($code == null) {
|
||||
$code = Protocol::CLOSE_UNEXPECTED;
|
||||
}
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
7
listeners/lib/Wrench/Exception/ConnectionException.php
Executable file
7
listeners/lib/Wrench/Exception/ConnectionException.php
Executable file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Exception;
|
||||
|
||||
class ConnectionException extends Exception
|
||||
{
|
||||
}
|
7
listeners/lib/Wrench/Exception/Exception.php
Executable file
7
listeners/lib/Wrench/Exception/Exception.php
Executable file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Exception;
|
||||
|
||||
class Exception extends \Exception
|
||||
{
|
||||
}
|
8
listeners/lib/Wrench/Exception/FrameException.php
Executable file
8
listeners/lib/Wrench/Exception/FrameException.php
Executable file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
namespace Wrench\Exception;
|
||||
|
||||
use Wrench\Exception\Exception as WrenchException;
|
||||
|
||||
class FrameException extends WrenchException
|
||||
{
|
||||
}
|
22
listeners/lib/Wrench/Exception/HandshakeException.php
Executable file
22
listeners/lib/Wrench/Exception/HandshakeException.php
Executable file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Exception;
|
||||
|
||||
use Wrench\Protocol\Protocol;
|
||||
use Wrench\Exception\Exception as WrenchException;
|
||||
|
||||
class HandshakeException extends WrenchException
|
||||
{
|
||||
/**
|
||||
* @param string $message
|
||||
* @param int $code
|
||||
* @param Exception $previous
|
||||
*/
|
||||
public function __construct($message = null, $code = null, $previous = null)
|
||||
{
|
||||
if ($code == null) {
|
||||
$code = Protocol::HTTP_SERVER_ERROR;
|
||||
}
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
25
listeners/lib/Wrench/Exception/InvalidOriginException.php
Executable file
25
listeners/lib/Wrench/Exception/InvalidOriginException.php
Executable file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Exception;
|
||||
|
||||
use Wrench\Protocol\Protocol;
|
||||
use Wrench\Exception\HandshakeException;
|
||||
|
||||
/**
|
||||
* Invalid origin exception
|
||||
*/
|
||||
class InvalidOriginException extends HandshakeException
|
||||
{
|
||||
/**
|
||||
* @param string $message
|
||||
* @param int $code
|
||||
* @param Exception $previous
|
||||
*/
|
||||
public function __construct($message = null, $code = null, $previous = null)
|
||||
{
|
||||
if ($code == null) {
|
||||
$code = Protocol::HTTP_FORBIDDEN;
|
||||
}
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
8
listeners/lib/Wrench/Exception/PayloadException.php
Executable file
8
listeners/lib/Wrench/Exception/PayloadException.php
Executable file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
namespace Wrench\Exception;
|
||||
|
||||
use Wrench\Exception\Exception as WrenchException;
|
||||
|
||||
class PayloadException extends WrenchException
|
||||
{
|
||||
}
|
20
listeners/lib/Wrench/Exception/RateLimiterException.php
Executable file
20
listeners/lib/Wrench/Exception/RateLimiterException.php
Executable file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
namespace Wrench\Exception;
|
||||
|
||||
use Wrench\Exception\Exception as WrenchException;
|
||||
|
||||
class RateLimiterException extends WrenchException
|
||||
{
|
||||
/**
|
||||
* @param string $message
|
||||
* @param int $code
|
||||
* @param Exception $previous
|
||||
*/
|
||||
public function __construct($message = null, $code = null, $previous = null)
|
||||
{
|
||||
if ($code == null) {
|
||||
$code = Protocol::CLOSE_GOING_AWAY;
|
||||
}
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
8
listeners/lib/Wrench/Exception/SocketException.php
Executable file
8
listeners/lib/Wrench/Exception/SocketException.php
Executable file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
namespace Wrench\Exception;
|
||||
|
||||
use Wrench\Exception\Exception as WrenchException;
|
||||
|
||||
class SocketException extends WrenchException
|
||||
{
|
||||
}
|
190
listeners/lib/Wrench/Frame/Frame.php
Executable file
190
listeners/lib/Wrench/Frame/Frame.php
Executable file
@ -0,0 +1,190 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Frame;
|
||||
|
||||
use Wrench\Payload\Payload;
|
||||
|
||||
use Wrench\Exception\FrameException;
|
||||
|
||||
/**
|
||||
* Represents a WebSocket frame
|
||||
*/
|
||||
abstract class Frame
|
||||
{
|
||||
/**
|
||||
* The frame data length
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $length = null;
|
||||
|
||||
/**
|
||||
* The type of this payload
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $type = null;
|
||||
|
||||
/**
|
||||
* The buffer
|
||||
*
|
||||
* May not be a complete payload, because this frame may still be receiving
|
||||
* data. See
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $buffer = '';
|
||||
|
||||
/**
|
||||
* The enclosed frame payload
|
||||
*
|
||||
* May not be a complete payload, because this frame might indicate a continuation
|
||||
* frame. See isFinal() versus isComplete()
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $payload = '';
|
||||
|
||||
/**
|
||||
* Gets the length of the payload
|
||||
*
|
||||
* @throws FrameException
|
||||
* @return int
|
||||
*/
|
||||
abstract public function getLength();
|
||||
|
||||
/**
|
||||
* Resets the frame and encodes the given data into it
|
||||
*
|
||||
* @param string $data
|
||||
* @param int $type
|
||||
* @param boolean $masked
|
||||
* @return Frame
|
||||
*/
|
||||
abstract public function encode($data, $type = Protocol::TYPE_TEXT, $masked = false);
|
||||
|
||||
/**
|
||||
* Whether the frame is the final one in a continuation
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
abstract public function isFinal();
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
abstract public function getType();
|
||||
|
||||
/**
|
||||
* Decodes a frame payload from the buffer
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function decodeFramePayloadFromBuffer();
|
||||
|
||||
/**
|
||||
* Gets the expected length of the buffer once all the data has been
|
||||
* receieved
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
abstract protected function getExpectedBufferLength();
|
||||
|
||||
/**
|
||||
* Whether the frame is complete
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isComplete()
|
||||
{
|
||||
if (!$this->buffer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->getBufferLength() >= $this->getExpectedBufferLength();
|
||||
} catch (FrameException $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Receieves data into the frame
|
||||
*
|
||||
* @param string $buffer
|
||||
*/
|
||||
public function receiveData($data)
|
||||
{
|
||||
$this->buffer .= $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the remaining number of bytes before this frame will be complete
|
||||
*
|
||||
* @return number
|
||||
*/
|
||||
public function getRemainingData()
|
||||
{
|
||||
try {
|
||||
return $this->getExpectedBufferLength() - $this->getBufferLength();
|
||||
} catch (FrameException $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this frame is waiting for more data
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isWaitingForData()
|
||||
{
|
||||
return $this->getRemainingData() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the contents of the frame payload
|
||||
*
|
||||
* The frame must be complete to call this method.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFramePayload()
|
||||
{
|
||||
if (!$this->isComplete()) {
|
||||
throw new FrameException('Cannot get payload: frame is not complete');
|
||||
}
|
||||
|
||||
if (!$this->payload && $this->buffer) {
|
||||
$this->decodeFramePayloadFromBuffer();
|
||||
}
|
||||
|
||||
return $this->payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the contents of the frame buffer
|
||||
*
|
||||
* This is the encoded value, receieved into the frame with recieveData().
|
||||
*
|
||||
* @throws FrameException
|
||||
* @return string binary
|
||||
*/
|
||||
public function getFrameBuffer()
|
||||
{
|
||||
if (!$this->buffer && $this->payload) {
|
||||
throw new FrameException('Cannot get frame buffer');
|
||||
}
|
||||
return $this->buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the expected length of the frame payload
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function getBufferLength()
|
||||
{
|
||||
return strlen($this->buffer);
|
||||
}
|
||||
}
|
376
listeners/lib/Wrench/Frame/HybiFrame.php
Executable file
376
listeners/lib/Wrench/Frame/HybiFrame.php
Executable file
@ -0,0 +1,376 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Frame;
|
||||
|
||||
use Wrench\Protocol\Protocol;
|
||||
use Wrench\Exception\FrameException;
|
||||
use \InvalidArgumentException;
|
||||
|
||||
class HybiFrame extends Frame
|
||||
{
|
||||
// First byte
|
||||
const BITFIELD_FINAL = 0x80;
|
||||
const BITFIELD_RSV1 = 0x40;
|
||||
const BITFIELD_RSV2 = 0x20;
|
||||
const BITFIELD_RSV3 = 0x10;
|
||||
const BITFIELD_TYPE = 0x0f;
|
||||
|
||||
// Second byte
|
||||
const BITFIELD_MASKED = 0x80;
|
||||
const BITFIELD_INITIAL_LENGTH = 0x7f;
|
||||
|
||||
// The inital byte offset before
|
||||
const BYTE_HEADER = 0;
|
||||
const BYTE_MASKED = 1;
|
||||
const BYTE_INITIAL_LENGTH = 1;
|
||||
|
||||
/**
|
||||
* Whether the payload is masked
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $masked = null;
|
||||
|
||||
/**
|
||||
* Masking key
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $mask = null;
|
||||
|
||||
/**
|
||||
* Byte offsets
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $offset_payload = null;
|
||||
protected $offset_mask = null;
|
||||
|
||||
/**
|
||||
* @see Wrench\Frame.Frame::encode()
|
||||
* ws-frame = frame-fin ; 1 bit in length
|
||||
* frame-rsv1 ; 1 bit in length
|
||||
* frame-rsv2 ; 1 bit in length
|
||||
* frame-rsv3 ; 1 bit in length
|
||||
* frame-opcode ; 4 bits in length
|
||||
* frame-masked ; 1 bit in length
|
||||
* frame-payload-length ; either 7, 7+16,
|
||||
* ; or 7+64 bits in
|
||||
* ; length
|
||||
* [ frame-masking-key ] ; 32 bits in length
|
||||
* frame-payload-data ; n*8 bits in
|
||||
* ; length, where
|
||||
* ; n >= 0
|
||||
*/
|
||||
public function encode($payload, $type = Protocol::TYPE_TEXT, $masked = false)
|
||||
{
|
||||
if (!is_int($type) || !in_array($type, Protocol::$frameTypes)) {
|
||||
throw new InvalidArgumentException('Invalid frame type');
|
||||
}
|
||||
|
||||
$this->type = $type;
|
||||
$this->masked = $masked;
|
||||
$this->payload = $payload;
|
||||
$this->length = strlen($this->payload);
|
||||
$this->offset_mask = null;
|
||||
$this->offset_payload = null;
|
||||
|
||||
$this->buffer = "\x00\x00";
|
||||
|
||||
$this->buffer[self::BYTE_HEADER] = chr(
|
||||
(self::BITFIELD_TYPE & $this->type)
|
||||
| (self::BITFIELD_FINAL & PHP_INT_MAX)
|
||||
);
|
||||
|
||||
$masked_bit = (self::BITFIELD_MASKED & ($this->masked ? PHP_INT_MAX : 0));
|
||||
|
||||
if ($this->length <= 125) {
|
||||
$this->buffer[self::BYTE_INITIAL_LENGTH] = chr(
|
||||
(self::BITFIELD_INITIAL_LENGTH & $this->length) | $masked_bit
|
||||
);
|
||||
} elseif ($this->length <= 65536) {
|
||||
$this->buffer[self::BYTE_INITIAL_LENGTH] = chr(
|
||||
(self::BITFIELD_INITIAL_LENGTH & 126) | $masked_bit
|
||||
);
|
||||
$this->buffer .= pack('n', $this->length);
|
||||
} else {
|
||||
$this->buffer[self::BYTE_INITIAL_LENGTH] = chr(
|
||||
(self::BITFIELD_INITIAL_LENGTH & 127) | $masked_bit
|
||||
);
|
||||
|
||||
if (PHP_INT_MAX > 2147483647) {
|
||||
$this->buffer .= pack('NN', $this->length >> 32, $this->length);
|
||||
// $this->buffer .= pack('I', $this->length);
|
||||
} else {
|
||||
$this->buffer .= pack('NN', 0, $this->length);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->masked) {
|
||||
$this->mask = $this->generateMask();
|
||||
$this->buffer .= $this->mask;
|
||||
$this->buffer .= $this->mask($this->payload);
|
||||
} else {
|
||||
$this->buffer .= $this->payload;
|
||||
}
|
||||
|
||||
$this->offset_mask = $this->getMaskOffset();
|
||||
$this->offset_payload = $this->getPayloadOffset();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Masks/Unmasks the frame
|
||||
*
|
||||
* @param string $payload
|
||||
* @return string
|
||||
*/
|
||||
protected function mask($payload)
|
||||
{
|
||||
$length = strlen($payload);
|
||||
$mask = $this->getMask();
|
||||
|
||||
$unmasked = '';
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$unmasked .= $payload[$i] ^ $mask[$i % 4];
|
||||
}
|
||||
|
||||
return $unmasked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Masks a payload
|
||||
*
|
||||
* @param string $payload
|
||||
* @return string
|
||||
*/
|
||||
protected function unmask($payload)
|
||||
{
|
||||
return $this->mask($payload);
|
||||
}
|
||||
|
||||
public function receiveData($data)
|
||||
{
|
||||
if ($this->getBufferLength() <= self::BYTE_INITIAL_LENGTH) {
|
||||
$this->length = null;
|
||||
$this->offset_payload = null;
|
||||
}
|
||||
parent::receiveData($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the mask
|
||||
*
|
||||
* @throws FrameException
|
||||
* @return string
|
||||
*/
|
||||
protected function getMask()
|
||||
{
|
||||
if (!isset($this->mask)) {
|
||||
if (!$this->isMasked()) {
|
||||
throw new FrameException('Cannot get mask: frame is not masked');
|
||||
}
|
||||
$this->mask = substr($this->buffer, $this->getMaskOffset(), $this->getMaskSize());
|
||||
}
|
||||
return $this->mask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a suitable masking key
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function generateMask()
|
||||
{
|
||||
if (extension_loaded('openssl')) {
|
||||
return openssl_random_pseudo_bytes(4);
|
||||
} else {
|
||||
// SHA1 is 128 bit (= 16 bytes)
|
||||
// So we pack it into 32 bits
|
||||
return pack('N', sha1(spl_object_hash($this) . mt_rand(0, PHP_INT_MAX) . uniqid('', true), true));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the frame is masked
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isMasked()
|
||||
{
|
||||
if (!isset($this->masked)) {
|
||||
if (!isset($this->buffer[1])) {
|
||||
throw new FrameException('Cannot tell if frame is masked: not enough frame data recieved');
|
||||
}
|
||||
$this->masked = (boolean)(ord($this->buffer[1]) & self::BITFIELD_MASKED);
|
||||
}
|
||||
return $this->masked;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Wrench\Frame.Frame::getExpectedDataLength()
|
||||
*/
|
||||
protected function getExpectedBufferLength()
|
||||
{
|
||||
return $this->getLength() + $this->getPayloadOffset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the offset of the payload in the frame
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function getPayloadOffset()
|
||||
{
|
||||
if (!isset($this->offset_payload)) {
|
||||
$offset = $this->getMaskOffset();
|
||||
$offset += $this->getMaskSize();
|
||||
|
||||
$this->offset_payload = $offset;
|
||||
}
|
||||
return $this->offset_payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the offset in the frame to the masking bytes
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function getMaskOffset()
|
||||
{
|
||||
if (!isset($this->offset_mask)) {
|
||||
$offset = self::BYTE_INITIAL_LENGTH + 1;
|
||||
$offset += $this->getLengthSize();
|
||||
|
||||
$this->offset_mask = $offset;
|
||||
}
|
||||
return $this->offset_mask;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Wrench\Frame.Frame::getLength()
|
||||
*/
|
||||
public function getLength()
|
||||
{
|
||||
if (!$this->length) {
|
||||
$initial = $this->getInitialLength();
|
||||
|
||||
if ($initial < 126) {
|
||||
$this->length = $initial;
|
||||
} elseif ($initial >= 126) {
|
||||
// Extended payload length: 2 or 8 bytes
|
||||
$start = self::BYTE_INITIAL_LENGTH + 1;
|
||||
$end = self::BYTE_INITIAL_LENGTH + $this->getLengthSize();
|
||||
|
||||
if ($end > $this->getBufferLength()) {
|
||||
throw new FrameException('Cannot get extended length: need more data');
|
||||
}
|
||||
|
||||
$length = 0;
|
||||
for ($i = $start; $i <= $end; $i++) {
|
||||
$length <<= 8;
|
||||
$length += ord($this->buffer[$i]);
|
||||
}
|
||||
|
||||
$this->length = $length;
|
||||
}
|
||||
}
|
||||
return $this->length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the inital length value, stored in the first length byte
|
||||
*
|
||||
* This determines how the rest of the length value is parsed out of the
|
||||
* frame.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function getInitialLength()
|
||||
{
|
||||
if (!isset($this->buffer[self::BYTE_INITIAL_LENGTH])) {
|
||||
throw new FrameException('Cannot yet tell expected length');
|
||||
}
|
||||
$a = (int)(ord($this->buffer[self::BYTE_INITIAL_LENGTH]) & self::BITFIELD_INITIAL_LENGTH);
|
||||
|
||||
return (int)(ord($this->buffer[self::BYTE_INITIAL_LENGTH]) & self::BITFIELD_INITIAL_LENGTH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the byte size of the length part of the frame
|
||||
*
|
||||
* Not including the initial 7 bit part
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function getLengthSize()
|
||||
{
|
||||
$initial = $this->getInitialLength();
|
||||
|
||||
if ($initial < 126) {
|
||||
return 0;
|
||||
} elseif ($initial === 126) {
|
||||
return 2;
|
||||
} elseif ($initial === 127) {
|
||||
return 8;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the byte size of the mask part of the frame
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function getMaskSize()
|
||||
{
|
||||
if ($this->isMasked()) {
|
||||
return 4;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Wrench\Frame.Frame::decodeFramePayloadFromBuffer()
|
||||
*/
|
||||
protected function decodeFramePayloadFromBuffer()
|
||||
{
|
||||
$payload = substr($this->buffer, $this->getPayloadOffset());
|
||||
|
||||
if ($this->isMasked()) {
|
||||
$payload = $this->unmask($payload);
|
||||
}
|
||||
|
||||
$this->payload = $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Wrench\Frame.Frame::isFinal()
|
||||
*/
|
||||
public function isFinal()
|
||||
{
|
||||
if (!isset($this->buffer[self::BYTE_HEADER])) {
|
||||
throw new FrameException('Cannot yet tell if frame is final');
|
||||
}
|
||||
return (boolean)(ord($this->buffer[self::BYTE_HEADER]) & self::BITFIELD_FINAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FrameException
|
||||
* @see Wrench\Frame.Frame::getType()
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
if (!isset($this->buffer[self::BYTE_HEADER])) {
|
||||
throw new FrameException('Cannot yet tell type of frame');
|
||||
}
|
||||
|
||||
$type = (int)(ord($this->buffer[self::BYTE_HEADER]) & self::BITFIELD_TYPE);
|
||||
|
||||
if (!in_array($type, Protocol::$frameTypes)) {
|
||||
throw new FrameException('Invalid payload type');
|
||||
}
|
||||
|
||||
return $type;
|
||||
}
|
||||
}
|
19
listeners/lib/Wrench/Listener/HandshakeRequestListener.php
Executable file
19
listeners/lib/Wrench/Listener/HandshakeRequestListener.php
Executable file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Listener;
|
||||
|
||||
use Wrench\Connection;
|
||||
|
||||
interface HandshakeRequestListener
|
||||
{
|
||||
/**
|
||||
* Handshake request listener
|
||||
*
|
||||
* @param Connection $connection
|
||||
* @param string $path
|
||||
* @param string $origin
|
||||
* @param string $key
|
||||
* @param array $extensions
|
||||
*/
|
||||
public function onHandshakeRequest(Connection $connection, $path, $origin, $key, $extensions);
|
||||
}
|
10
listeners/lib/Wrench/Listener/Listener.php
Executable file
10
listeners/lib/Wrench/Listener/Listener.php
Executable file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Listener;
|
||||
|
||||
use Wrench\Server;
|
||||
|
||||
interface Listener
|
||||
{
|
||||
public function listen(Server $server);
|
||||
}
|
76
listeners/lib/Wrench/Listener/OriginPolicy.php
Executable file
76
listeners/lib/Wrench/Listener/OriginPolicy.php
Executable file
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Listener;
|
||||
|
||||
use Wrench\Connection;
|
||||
use Wrench\Exception\InvalidOriginException;
|
||||
use Wrench\Server;
|
||||
|
||||
class OriginPolicy implements Listener, HandshakeRequestListener
|
||||
{
|
||||
protected $allowed = array();
|
||||
|
||||
public function __construct(array $allowed)
|
||||
{
|
||||
$this->allowed = $allowed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handshake request listener
|
||||
*
|
||||
* Closes the connection on handshake from an origin that isn't allowed
|
||||
*
|
||||
* @param Connection $connection
|
||||
* @param string $path
|
||||
* @param string $origin
|
||||
* @param string $key
|
||||
* @param array $extensions
|
||||
*/
|
||||
public function onHandshakeRequest(Connection $connection, $path, $origin, $key, $extensions)
|
||||
{
|
||||
if (!$this->isAllowed($origin)) {
|
||||
$connection->close(new InvalidOriginException('Origin not allowed'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the specified origin is allowed under this policy
|
||||
*
|
||||
* @param string $origin
|
||||
* @return boolean
|
||||
*/
|
||||
public function isAllowed($origin)
|
||||
{
|
||||
$scheme = parse_url($origin, PHP_URL_SCHEME);
|
||||
$host = parse_url($origin, PHP_URL_HOST) ?: $origin;
|
||||
|
||||
foreach ($this->allowed as $allowed) {
|
||||
$allowed_scheme = parse_url($allowed, PHP_URL_SCHEME);
|
||||
|
||||
if ($allowed_scheme && $scheme != $allowed_scheme) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$allowed_host = parse_url($allowed, PHP_URL_HOST) ?: $allowed;
|
||||
|
||||
if ($host != $allowed_host) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Server $server
|
||||
*/
|
||||
public function listen(Server $server)
|
||||
{
|
||||
$server->addListener(
|
||||
Server::EVENT_HANDSHAKE_REQUEST,
|
||||
array($this, 'onHandshakeRequest')
|
||||
);
|
||||
}
|
||||
}
|
230
listeners/lib/Wrench/Listener/RateLimiter.php
Executable file
230
listeners/lib/Wrench/Listener/RateLimiter.php
Executable file
@ -0,0 +1,230 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Listener;
|
||||
|
||||
use Wrench\Util\Configurable;
|
||||
use Wrench\Server;
|
||||
|
||||
class RateLimiter extends Configurable implements Listener
|
||||
{
|
||||
/**
|
||||
* The server being limited
|
||||
*
|
||||
* @var Server
|
||||
*/
|
||||
protected $server;
|
||||
|
||||
/**
|
||||
* Connection counts per IP address
|
||||
*
|
||||
* @var array<int>
|
||||
*/
|
||||
protected $ips = array();
|
||||
|
||||
/**
|
||||
* Request tokens per IP address
|
||||
*
|
||||
* @var array<array<int>>
|
||||
*/
|
||||
protected $requests = array();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param array $options
|
||||
*/
|
||||
public function __construct(array $options = array())
|
||||
{
|
||||
parent::__construct($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $options
|
||||
*/
|
||||
protected function configure(array $options)
|
||||
{
|
||||
$options = array_merge(array(
|
||||
'connections' => 200, // Total
|
||||
'connections_per_ip' => 5, // At once
|
||||
'requests_per_minute' => 200 // Per connection
|
||||
), $options);
|
||||
|
||||
parent::configure($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Wrench\Listener.Listener::listen()
|
||||
*/
|
||||
public function listen(Server $server)
|
||||
{
|
||||
$this->server = $server;
|
||||
|
||||
$server->addListener(
|
||||
Server::EVENT_SOCKET_CONNECT,
|
||||
array($this, 'onSocketConnect')
|
||||
);
|
||||
|
||||
$server->addListener(
|
||||
Server::EVENT_SOCKET_DISCONNECT,
|
||||
array($this, 'onSocketDisconnect')
|
||||
);
|
||||
|
||||
$server->addListener(
|
||||
Server::EVENT_CLIENT_DATA,
|
||||
array($this, 'onClientData')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener
|
||||
*
|
||||
* @param resource $socket
|
||||
* @param Connection $connection
|
||||
*/
|
||||
public function onSocketConnect($socket, $connection)
|
||||
{
|
||||
$this->checkConnections($connection);
|
||||
$this->checkConnectionsPerIp($connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener
|
||||
*
|
||||
* @param resource $socket
|
||||
* @param Connection $connection
|
||||
*/
|
||||
public function onSocketDisconnect($socket, $connection)
|
||||
{
|
||||
$this->releaseConnection($connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener
|
||||
*
|
||||
* @param resource $socket
|
||||
* @param Connection $connection
|
||||
*/
|
||||
public function onClientData($socket, $connection)
|
||||
{
|
||||
$this->checkRequestsPerMinute($connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Idempotent
|
||||
*
|
||||
* @param Connection $connection
|
||||
*/
|
||||
protected function checkConnections($connection)
|
||||
{
|
||||
$connections = $connection->getConnectionManager()->count();
|
||||
|
||||
if ($connections > $this->options['connections']) {
|
||||
$this->limit($connection, 'Max connections');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NOT idempotent, call once per connection
|
||||
*
|
||||
* @param Connection $connection
|
||||
*/
|
||||
protected function checkConnectionsPerIp($connection)
|
||||
{
|
||||
$ip = $connection->getIp();
|
||||
|
||||
if (!$ip) {
|
||||
$this->log('Cannot check connections per IP', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isset($this->ips[$ip])) {
|
||||
$this->ips[$ip] = 1;
|
||||
} else {
|
||||
$this->ips[$ip] = min(
|
||||
$this->options['connections_per_ip'],
|
||||
$this->ips[$ip] + 1
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->ips[$ip] > $this->options['connections_per_ip']) {
|
||||
$this->limit($connection, 'Connections per IP');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NOT idempotent, call once per disconnection
|
||||
*
|
||||
* @param Connection $connection
|
||||
*/
|
||||
protected function releaseConnection($connection)
|
||||
{
|
||||
$ip = $connection->getIp();
|
||||
|
||||
if (!$ip) {
|
||||
$this->log('Cannot release connection', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isset($this->ips[$ip])) {
|
||||
$this->ips[$ip] = 0;
|
||||
} else {
|
||||
$this->ips[$ip] = max(0, $this->ips[$ip] - 1);
|
||||
}
|
||||
|
||||
unset($this->requests[$connection->getId()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* NOT idempotent, call once per data
|
||||
*
|
||||
* @param Connection $connection
|
||||
*/
|
||||
protected function checkRequestsPerMinute($connection)
|
||||
{
|
||||
$id = $connection->getId();
|
||||
|
||||
if (!isset($this->requests[$id])) {
|
||||
$this->requests[$id] = array();
|
||||
}
|
||||
|
||||
// Add current token
|
||||
$this->requests[$id][] = time();
|
||||
|
||||
// Expire old tokens
|
||||
while (reset($this->requests[$id]) < time() - 60) {
|
||||
array_shift($this->requests[$id]);
|
||||
}
|
||||
|
||||
if (count($this->requests[$id]) > $this->options['requests_per_minute']) {
|
||||
$this->limit($connection, 'Requests per minute');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Limits the given connection
|
||||
*
|
||||
* @param Connection $connection
|
||||
* @param string $limit Reason
|
||||
*/
|
||||
protected function limit($connection, $limit)
|
||||
{
|
||||
$this->log(sprintf(
|
||||
'Limiting connection %s: %s',
|
||||
$connection->getIp(),
|
||||
$limit
|
||||
), 'notice');
|
||||
|
||||
$connection->close(new RateLimiterException($limit));
|
||||
}
|
||||
|
||||
/**
|
||||
* Logger
|
||||
*
|
||||
* @param string $message
|
||||
* @param string $priority
|
||||
*/
|
||||
public function log($message, $priority = 'info')
|
||||
{
|
||||
$this->server->log('RateLimiter: ' . $message, $priority);
|
||||
}
|
||||
}
|
22
listeners/lib/Wrench/Payload/HybiPayload.php
Executable file
22
listeners/lib/Wrench/Payload/HybiPayload.php
Executable file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Payload;
|
||||
|
||||
use Wrench\Frame\HybiFrame;
|
||||
use Wrench\Exception\PayloadException;
|
||||
|
||||
/**
|
||||
* Gets a HyBi payload
|
||||
* @author Dominic
|
||||
*
|
||||
*/
|
||||
class HybiPayload extends Payload
|
||||
{
|
||||
/**
|
||||
* @see Wrench\Payload.Payload::getFrame()
|
||||
*/
|
||||
protected function getFrame()
|
||||
{
|
||||
return new HybiFrame();
|
||||
}
|
||||
}
|
217
listeners/lib/Wrench/Payload/Payload.php
Executable file
217
listeners/lib/Wrench/Payload/Payload.php
Executable file
@ -0,0 +1,217 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Payload;
|
||||
|
||||
use Wrench\Frame\Frame;
|
||||
|
||||
use Wrench\Exception\FrameException;
|
||||
|
||||
use Wrench\Socket\Socket;
|
||||
|
||||
/**
|
||||
* Payload class
|
||||
*
|
||||
* Represents a WebSocket protocol payload, which may be made up of multiple
|
||||
* frames.
|
||||
*/
|
||||
abstract class Payload
|
||||
{
|
||||
/**
|
||||
* A payload may consist of one or more frames
|
||||
*
|
||||
* @var array<Frame>
|
||||
*/
|
||||
protected $frames = array();
|
||||
|
||||
/**
|
||||
* Gets the current frame for the payload
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getCurrentFrame()
|
||||
{
|
||||
if (empty($this->frames)) {
|
||||
array_push($this->frames, $this->getFrame());
|
||||
}
|
||||
return end($this->frames);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the frame into which data should be receieved
|
||||
*
|
||||
* @throws PayloadException
|
||||
* @return Frame
|
||||
*/
|
||||
protected function getReceivingFrame()
|
||||
{
|
||||
$current = $this->getCurrentFrame();
|
||||
|
||||
if ($current->isComplete()) {
|
||||
if ($current->isFinal()) {
|
||||
throw new PayloadException('Payload cannot receieve data: it is already complete');
|
||||
} else {
|
||||
$current = array_push($this->frames, $this->getFrame());
|
||||
}
|
||||
}
|
||||
|
||||
return $current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a frame object
|
||||
*
|
||||
* @return Frame
|
||||
*/
|
||||
abstract protected function getFrame();
|
||||
|
||||
/**
|
||||
* Whether the payload is complete
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isComplete()
|
||||
{
|
||||
return $this->getCurrentFrame()->isComplete() && $this->getCurrentFrame()->isFinal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a payload
|
||||
*
|
||||
* @param string $data
|
||||
* @param int $type
|
||||
* @param boolean $masked
|
||||
* @return Payload
|
||||
* @todo No splitting into multiple frames just yet
|
||||
*/
|
||||
public function encode($data, $type = Protocol::TYPE_TEXT, $masked = false)
|
||||
{
|
||||
$this->frames = array();
|
||||
|
||||
$frame = $this->getFrame();
|
||||
array_push($this->frames, $frame);
|
||||
|
||||
$frame->encode($data, $type, $masked);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of remaining bytes before this payload will be
|
||||
* complete
|
||||
*
|
||||
* May return 0 (no more bytes required) or null (unknown number of bytes
|
||||
* required).
|
||||
*
|
||||
* @return number|NULL
|
||||
*/
|
||||
public function getRemainingData()
|
||||
{
|
||||
if ($this->isComplete()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($this->getCurrentFrame()->isFinal()) {
|
||||
return $this->getCurrentFrame()->getRemainingData();
|
||||
}
|
||||
} catch (FrameException $e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this payload is waiting for more data
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isWaitingForData()
|
||||
{
|
||||
return $this->getRemainingData() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Socket $socket
|
||||
* @return boolean
|
||||
*/
|
||||
public function sendToSocket(Socket $socket)
|
||||
{
|
||||
$success = true;
|
||||
foreach ($this->frames as $frame) {
|
||||
$success = $success && ($socket->send($frame->getFrameBuffer()) !== false);
|
||||
}
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive raw data into the payload
|
||||
*
|
||||
* @param string $data
|
||||
* @return void
|
||||
*/
|
||||
public function receiveData($data)
|
||||
{
|
||||
while ($data) {
|
||||
$frame = $this->getReceivingFrame();
|
||||
|
||||
$size = strlen($data);
|
||||
$remaining = $frame->getRemainingData();
|
||||
|
||||
if ($remaining === null) {
|
||||
$chunk_size = 2;
|
||||
} elseif ($remaining > 0) {
|
||||
$chunk_size = $remaining;
|
||||
}
|
||||
|
||||
$chunk_size = min(strlen($data), $chunk_size);
|
||||
$chunk = substr($data, 0, $chunk_size);
|
||||
$data = substr($data, $chunk_size);
|
||||
|
||||
$frame->receiveData($chunk);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getPayload()
|
||||
{
|
||||
$this->buffer = '';
|
||||
|
||||
foreach ($this->frames as $frame) {
|
||||
$this->buffer .= $frame->getFramePayload();
|
||||
}
|
||||
|
||||
return $this->buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
try {
|
||||
return $this->getPayload();
|
||||
} catch (\Exception $e) {
|
||||
// __toString must not throw an exception
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the type of the payload
|
||||
*
|
||||
* The type of a payload is taken from its first frame
|
||||
*
|
||||
* @throws PayloadException
|
||||
* @return int
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
if (!isset($this->frames[0])) {
|
||||
throw new PayloadException('Cannot tell payload type yet');
|
||||
}
|
||||
return $this->frames[0]->getType();
|
||||
}
|
||||
}
|
110
listeners/lib/Wrench/Payload/PayloadHandler.php
Executable file
110
listeners/lib/Wrench/Payload/PayloadHandler.php
Executable file
@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Payload;
|
||||
|
||||
use Wrench\Exception\PayloadException;
|
||||
use Wrench\Exception\ConnectionException;
|
||||
use Wrench\Util\Configurable;
|
||||
use \InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Handles chunking and splitting of payloads into frames
|
||||
*/
|
||||
class PayloadHandler extends Configurable
|
||||
{
|
||||
/**
|
||||
* A callback that will be called when a complete payload is available
|
||||
*
|
||||
* @var callable
|
||||
*/
|
||||
protected $callback;
|
||||
|
||||
/**
|
||||
* The current payload
|
||||
*/
|
||||
protected $payload;
|
||||
|
||||
/**
|
||||
* @param callable $callback
|
||||
* @param array $options
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __construct($callback, array $options = array())
|
||||
{
|
||||
parent::__construct($options);
|
||||
|
||||
if (!is_callable($callback)) {
|
||||
throw new InvalidArgumentException('You must supply a callback to PayloadHandler');
|
||||
}
|
||||
|
||||
$this->callback = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the raw socket data given
|
||||
*
|
||||
* @param string $data
|
||||
*/
|
||||
public function handle($data)
|
||||
{
|
||||
if (!$this->payload) {
|
||||
$this->payload = $this->protocol->getPayload();
|
||||
}
|
||||
|
||||
while ($data) { // Each iteration pulls off a single payload chunk
|
||||
$size = strlen($data);
|
||||
$remaining = $this->payload->getRemainingData();
|
||||
|
||||
// If we don't yet know how much data is remaining, read data into
|
||||
// the payload in two byte chunks (the size of a WebSocket frame
|
||||
// header to get the initial length)
|
||||
//
|
||||
// Then re-loop. For extended lengths, this will happen once or four
|
||||
// times extra, as the extended length is read in.
|
||||
if ($remaining === null) {
|
||||
$chunk_size = 2;
|
||||
} elseif ($remaining > 0) {
|
||||
$chunk_size = $remaining;
|
||||
} elseif ($remaining === 0) {
|
||||
$chunk_size = 0;
|
||||
}
|
||||
|
||||
$chunk_size = min(strlen($data), $chunk_size);
|
||||
$chunk = substr($data, 0, $chunk_size);
|
||||
$data = substr($data, $chunk_size);
|
||||
|
||||
$this->payload->receiveData($chunk);
|
||||
|
||||
if ($remaining !== 0 && !$this->payload->isComplete()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->payload->isComplete()) {
|
||||
$this->emit($this->payload);
|
||||
$this->payload = $this->protocol->getPayload();
|
||||
} else {
|
||||
throw new PayloadException('Payload will not complete');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current payload
|
||||
*
|
||||
* @return Payload
|
||||
*/
|
||||
public function getCurrent()
|
||||
{
|
||||
return $this->getPayloadHandler->getCurrent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits a complete payload to the callback
|
||||
*
|
||||
* @param Payload $payload
|
||||
*/
|
||||
protected function emit(Payload $payload)
|
||||
{
|
||||
call_user_func($this->callback, $payload);
|
||||
}
|
||||
}
|
35
listeners/lib/Wrench/Protocol/Hybi10Protocol.php
Executable file
35
listeners/lib/Wrench/Protocol/Hybi10Protocol.php
Executable file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Protocol;
|
||||
|
||||
use Wrench\Protocol\HybiProtocol;
|
||||
|
||||
/**
|
||||
* http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10
|
||||
*/
|
||||
class Hybi10Protocol extends HybiProtocol
|
||||
{
|
||||
const VERSION = 10;
|
||||
|
||||
/**
|
||||
* @see Wrench\Protocol.Protocol::getVersion()
|
||||
*/
|
||||
public function getVersion()
|
||||
{
|
||||
return self::VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is our most recent protocol class
|
||||
*
|
||||
* @see Wrench\Protocol.Protocol::acceptsVersion()
|
||||
*/
|
||||
public function acceptsVersion($version)
|
||||
{
|
||||
$version = (int)$version;
|
||||
|
||||
if ($version <= 10 && $version >= 10) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
24
listeners/lib/Wrench/Protocol/HybiProtocol.php
Executable file
24
listeners/lib/Wrench/Protocol/HybiProtocol.php
Executable file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Protocol;
|
||||
|
||||
use Wrench\Payload\HybiPayload;
|
||||
|
||||
use Wrench\Exception\ConnectionException;
|
||||
|
||||
use Wrench\Protocol\Protocol;
|
||||
use \InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* @see http://tools.ietf.org/html/rfc6455#section-5.2
|
||||
*/
|
||||
abstract class HybiProtocol extends Protocol
|
||||
{
|
||||
/**
|
||||
* @see Wrench\Protocol.Protocol::getPayload()
|
||||
*/
|
||||
public function getPayload()
|
||||
{
|
||||
return new HybiPayload();
|
||||
}
|
||||
}
|
791
listeners/lib/Wrench/Protocol/Protocol.php
Executable file
791
listeners/lib/Wrench/Protocol/Protocol.php
Executable file
@ -0,0 +1,791 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Protocol;
|
||||
|
||||
use Wrench\Payload\Payload;
|
||||
|
||||
use Wrench\Exception\BadRequestException;
|
||||
|
||||
use \Exception;
|
||||
use \InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Definitions and implementation helpers for the Wrenchs protocol
|
||||
*
|
||||
* Based on RFC 6455: http://tools.ietf.org/html/rfc6455
|
||||
*/
|
||||
abstract class Protocol
|
||||
{
|
||||
/**#@+
|
||||
* Relevant schemes
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const SCHEME_WEBSOCKET = 'ws';
|
||||
const SCHEME_WEBSOCKET_SECURE = 'wss';
|
||||
const SCHEME_UNDERLYING = 'tcp';
|
||||
const SCHEME_UNDERLYING_SECURE = 'tls';
|
||||
/**#@-*/
|
||||
|
||||
/**#@+
|
||||
* HTTP headers
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const HEADER_HOST = 'Host';
|
||||
const HEADER_KEY = 'Sec-WebSocket-Key';
|
||||
const HEADER_PROTOCOL = 'Sec-WebSocket-Protocol';
|
||||
const HEADER_VERSION = 'Sec-WebSocket-Version';
|
||||
const HEADER_ACCEPT = 'Sec-WebSocket-Accept';
|
||||
const HEADER_EXTENSIONS = 'Sec-WebSocket-Extensions';
|
||||
const HEADER_ORIGIN = 'Origin';
|
||||
const HEADER_CONNECTION = 'Connection';
|
||||
const HEADER_UPGRADE = 'Upgrade';
|
||||
/**#@-*/
|
||||
|
||||
/**#@+
|
||||
* HTTP error statuses
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const HTTP_SWITCHING_PROTOCOLS = 101;
|
||||
const HTTP_BAD_REQUEST = 400;
|
||||
const HTTP_UNAUTHORIZED = 401;
|
||||
const HTTP_FORBIDDEN = 403;
|
||||
const HTTP_NOT_FOUND = 404;
|
||||
const HTTP_RATE_LIMITED = 420;
|
||||
const HTTP_SERVER_ERROR = 500;
|
||||
const HTTP_NOT_IMPLEMENTED = 501;
|
||||
/**#@-*/
|
||||
|
||||
/**#@+
|
||||
* Close statuses
|
||||
*
|
||||
* @see http://tools.ietf.org/html/rfc6455#section-7.4
|
||||
* @var int
|
||||
*/
|
||||
const CLOSE_NORMAL = 1000;
|
||||
const CLOSE_GOING_AWAY = 1001;
|
||||
const CLOSE_PROTOCOL_ERROR = 1002;
|
||||
const CLOSE_DATA_INVALID = 1003;
|
||||
const CLOSE_RESERVED = 1004;
|
||||
const CLOSE_RESERVED_NONE = 1005;
|
||||
const CLOSE_RESERVED_ABNORM = 1006;
|
||||
const CLOSE_DATA_INCONSISTENT = 1007;
|
||||
const CLOSE_POLICY_VIOLATION = 1008;
|
||||
const CLOSE_MESSAGE_TOO_BIG = 1009;
|
||||
const CLOSE_EXTENSION_NEEDED = 1010;
|
||||
const CLOSE_UNEXPECTED = 1011;
|
||||
const CLOSE_RESERVED_TLS = 1015;
|
||||
/**#@-*/
|
||||
|
||||
/**#@+
|
||||
* Frame types
|
||||
*
|
||||
* %x0 denotes a continuation frame
|
||||
* %x1 denotes a text frame
|
||||
* %x2 denotes a binary frame
|
||||
* %x3-7 are reserved for further non-control frames
|
||||
* %x8 denotes a connection close
|
||||
* %x9 denotes a ping
|
||||
* %xA denotes a pong
|
||||
* %xB-F are reserved for further control frames
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const TYPE_CONTINUATION = 0;
|
||||
const TYPE_TEXT = 1;
|
||||
const TYPE_BINARY = 2;
|
||||
const TYPE_RESERVED_3 = 3;
|
||||
const TYPE_RESERVED_4 = 4;
|
||||
const TYPE_RESERVED_5 = 5;
|
||||
const TYPE_RESERVED_6 = 6;
|
||||
const TYPE_RESERVED_7 = 7;
|
||||
const TYPE_CLOSE = 8;
|
||||
const TYPE_PING = 9;
|
||||
const TYPE_PONG = 10;
|
||||
const TYPE_RESERVED_11 = 11;
|
||||
const TYPE_RESERVED_12 = 12;
|
||||
const TYPE_RESERVED_13 = 13;
|
||||
const TYPE_RESERVED_14 = 14;
|
||||
const TYPE_RESERVED_15 = 15;
|
||||
/**#@-*/
|
||||
|
||||
/**
|
||||
* Magic GUID
|
||||
*
|
||||
* Used in the WebSocket accept header
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const MAGIC_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
||||
|
||||
/**
|
||||
* The request MUST contain an |Upgrade| header field whose value
|
||||
* MUST include the "websocket" keyword.
|
||||
*/
|
||||
const UPGRADE_VALUE = 'websocket';
|
||||
|
||||
/**
|
||||
* The request MUST contain a |Connection| header field whose value
|
||||
* MUST include the "Upgrade" token.
|
||||
*/
|
||||
const CONNECTION_VALUE = 'Upgrade';
|
||||
|
||||
/**
|
||||
* Request line format
|
||||
*
|
||||
* @var string printf compatible, passed request path string
|
||||
*/
|
||||
const REQUEST_LINE_FORMAT = 'GET %s HTTP/1.1';
|
||||
|
||||
/**
|
||||
* Request line regex
|
||||
*
|
||||
* Used for parsing requested path
|
||||
*
|
||||
* @var string preg_* compatible
|
||||
*/
|
||||
const REQUEST_LINE_REGEX = '/^GET (\S+) HTTP\/1.1$/';
|
||||
|
||||
/**
|
||||
* Response line format
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const RESPONSE_LINE_FORMAT = 'HTTP/1.1 %d %s';
|
||||
|
||||
/**
|
||||
* Header line format
|
||||
*
|
||||
* @var string printf compatible, passed header name and value
|
||||
*/
|
||||
const HEADER_LINE_FORMAT = '%s: %s';
|
||||
|
||||
/**
|
||||
* Valid schemes
|
||||
*
|
||||
* @var array<string>
|
||||
*/
|
||||
protected static $schemes = array(
|
||||
self::SCHEME_WEBSOCKET,
|
||||
self::SCHEME_WEBSOCKET_SECURE,
|
||||
self::SCHEME_UNDERLYING,
|
||||
self::SCHEME_UNDERLYING_SECURE
|
||||
);
|
||||
|
||||
/**
|
||||
* Close status codes
|
||||
*
|
||||
* @var array<int => string>
|
||||
*/
|
||||
public static $closeReasons = array(
|
||||
self::CLOSE_NORMAL => 'normal close',
|
||||
self::CLOSE_GOING_AWAY => 'going away',
|
||||
self::CLOSE_PROTOCOL_ERROR => 'protocol error',
|
||||
self::CLOSE_DATA_INVALID => 'data invalid',
|
||||
self::CLOSE_DATA_INCONSISTENT => 'data inconsistent',
|
||||
self::CLOSE_POLICY_VIOLATION => 'policy violation',
|
||||
self::CLOSE_MESSAGE_TOO_BIG => 'message too big',
|
||||
self::CLOSE_EXTENSION_NEEDED => 'extension needed',
|
||||
self::CLOSE_UNEXPECTED => 'unexpected error',
|
||||
self::CLOSE_RESERVED => null, // Don't use these!
|
||||
self::CLOSE_RESERVED_NONE => null,
|
||||
self::CLOSE_RESERVED_ABNORM => null,
|
||||
self::CLOSE_RESERVED_TLS => null
|
||||
);
|
||||
|
||||
/**
|
||||
* Frame types
|
||||
*
|
||||
* @todo flip values and keys?
|
||||
* @var array<string => int>
|
||||
*/
|
||||
public static $frameTypes = array(
|
||||
'continuation' => self::TYPE_CONTINUATION,
|
||||
'text' => self::TYPE_TEXT,
|
||||
'binary' => self::TYPE_BINARY,
|
||||
'close' => self::TYPE_CLOSE,
|
||||
'ping' => self::TYPE_PING,
|
||||
'pong' => self::TYPE_PONG
|
||||
);
|
||||
|
||||
/**
|
||||
* HTTP errors
|
||||
*
|
||||
* @var array<int => string>
|
||||
*/
|
||||
public static $httpResponses = array(
|
||||
self::HTTP_SWITCHING_PROTOCOLS => 'Switching Protocols',
|
||||
self::HTTP_BAD_REQUEST => 'Bad Request',
|
||||
self::HTTP_UNAUTHORIZED => 'Unauthorized',
|
||||
self::HTTP_FORBIDDEN => 'Forbidden',
|
||||
self::HTTP_NOT_FOUND => 'Not Found',
|
||||
self::HTTP_NOT_IMPLEMENTED => 'Not Implemented',
|
||||
self::HTTP_RATE_LIMITED => 'Enhance Your Calm'
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets a version number
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
abstract public function getVersion();
|
||||
|
||||
/**
|
||||
* Subclasses should implement this method and return a boolean to the given
|
||||
* version string, as to whether they would like to accept requests from
|
||||
* user agents that specify that version.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
abstract public function acceptsVersion($version);
|
||||
|
||||
/**
|
||||
* Gets a payload instance, suitable for use in decoding/encoding protocol
|
||||
* frames
|
||||
*
|
||||
* @return Payload
|
||||
*/
|
||||
abstract public function getPayload();
|
||||
|
||||
/**
|
||||
* Generates a key suitable for use in the protocol
|
||||
*
|
||||
* This base implementation returns a 16-byte (128 bit) random key as a
|
||||
* binary string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function generateKey()
|
||||
{
|
||||
if (extension_loaded('openssl')) {
|
||||
$key = openssl_random_pseudo_bytes(16);
|
||||
} else {
|
||||
// SHA1 is 128 bit (= 16 bytes)
|
||||
$key = sha1(spl_object_hash($this) . mt_rand(0, PHP_INT_MAX) . uniqid('', true), true);
|
||||
}
|
||||
|
||||
return base64_encode($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets request handshake string
|
||||
*
|
||||
* The leading line from the client follows the Request-Line format.
|
||||
* The leading line from the server follows the Status-Line format. The
|
||||
* Request-Line and Status-Line productions are defined in [RFC2616].
|
||||
*
|
||||
* An unordered set of header fields comes after the leading line in
|
||||
* both cases. The meaning of these header fields is specified in
|
||||
* Section 4 of this document. Additional header fields may also be
|
||||
* present, such as cookies [RFC6265]. The format and parsing of
|
||||
* headers is as defined in [RFC2616].
|
||||
*
|
||||
* @param string $uri WebSocket URI, e.g. ws://example.org:8000/chat
|
||||
* @param string $key 16 byte binary string key
|
||||
* @param string $origin Origin of the request
|
||||
* @return string
|
||||
*/
|
||||
public function getRequestHandshake(
|
||||
$uri,
|
||||
$key,
|
||||
$origin,
|
||||
array $headers = array()
|
||||
) {
|
||||
if (!$uri || !$key || !$origin) {
|
||||
throw new InvalidArgumentException('You must supply a URI, key and origin');
|
||||
}
|
||||
|
||||
list($scheme, $host, $port, $path) = self::validateUri($uri);
|
||||
|
||||
$handshake = array(
|
||||
sprintf(self::REQUEST_LINE_FORMAT, $path)
|
||||
);
|
||||
|
||||
$headers = array_merge(
|
||||
$this->getDefaultRequestHeaders(
|
||||
$host . ':' . $port, $key, $origin
|
||||
),
|
||||
$headers
|
||||
);
|
||||
|
||||
foreach ($headers as $name => $value) {
|
||||
$handshake[] = sprintf(self::HEADER_LINE_FORMAT, $name, $value);
|
||||
}
|
||||
return implode($handshake, "\r\n") . "\r\n\r\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a handshake response body
|
||||
*
|
||||
* @param string $key
|
||||
* @param array $headers
|
||||
*/
|
||||
public function getResponseHandshake($key, array $headers = array())
|
||||
{
|
||||
$headers = array_merge(
|
||||
$this->getSuccessResponseHeaders(
|
||||
$key
|
||||
),
|
||||
$headers
|
||||
);
|
||||
|
||||
return $this->getHttpResponse(self::HTTP_SWITCHING_PROTOCOLS, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a response to an error in the handshake
|
||||
*
|
||||
* @param int|Exception $e Exception or HTTP error
|
||||
* @param array $headers
|
||||
*/
|
||||
public function getResponseError($e, array $headers = array())
|
||||
{
|
||||
$code = false;
|
||||
|
||||
if ($e instanceof Exception) {
|
||||
$code = $e->getCode();
|
||||
} elseif (is_numeric($e)) {
|
||||
$code = (int)$e;
|
||||
}
|
||||
|
||||
if (!$code || $code < 400 || $code > 599) {
|
||||
$code = self::HTTP_SERVER_ERROR;
|
||||
}
|
||||
|
||||
return $this->getHttpResponse($code, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an HTTP response
|
||||
*
|
||||
* @param int $status
|
||||
* @param array $headers
|
||||
*/
|
||||
protected function getHttpResponse($status, array $headers = array())
|
||||
{
|
||||
if (array_key_exists($status, self::$httpResponses)) {
|
||||
$response = self::$httpResponses[$status];
|
||||
} else {
|
||||
$response = self::$httpResponses[self::HTTP_NOT_IMPLEMENTED];
|
||||
}
|
||||
|
||||
$handshake = array(
|
||||
sprintf(self::RESPONSE_LINE_FORMAT, $status, $response)
|
||||
);
|
||||
|
||||
foreach ($headers as $name => $value) {
|
||||
$handshake[] = sprintf(self::HEADER_LINE_FORMAT, $name, $value);
|
||||
}
|
||||
|
||||
return implode($handshake, "\r\n") . "\r\n\r\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo better header handling
|
||||
* @todo throw exception
|
||||
* @param unknown_type $response
|
||||
* @param unknown_type $key
|
||||
* @return boolean
|
||||
*/
|
||||
public function validateResponseHandshake($response, $key)
|
||||
{
|
||||
if (!$response) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$headers = $this->getHeaders($response);
|
||||
|
||||
if (!isset($headers[self::HEADER_ACCEPT])) {
|
||||
throw new HandshakeException('No accept header receieved on handshake response');
|
||||
}
|
||||
|
||||
$accept = $headers[self::HEADER_ACCEPT];
|
||||
|
||||
if (!$accept) {
|
||||
throw new HandshakeException('Invalid accept header');
|
||||
}
|
||||
|
||||
$expected = $this->getAcceptValue($key);
|
||||
|
||||
preg_match('#Sec-WebSocket-Accept:\s(.*)$#mU', $response, $matches);
|
||||
$keyAccept = trim($matches[1]);
|
||||
|
||||
return ($keyAccept === $this->getEncodedHash($key)) ? true : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an encoded hash for a key
|
||||
*
|
||||
* @param string $key
|
||||
* @return string
|
||||
*/
|
||||
public function getEncodedHash($key)
|
||||
{
|
||||
return base64_encode(pack('H*', sha1($key . self::MAGIC_GUID)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a request handshake
|
||||
*
|
||||
* @param string $request
|
||||
* @throws BadRequestException
|
||||
*/
|
||||
public function validateRequestHandshake(
|
||||
$request
|
||||
) {
|
||||
if (!$request) {
|
||||
return false;
|
||||
}
|
||||
|
||||
list($request, $headers) = $this->getRequestHeaders($request);
|
||||
|
||||
$path = $this->validateRequestLine($request);
|
||||
|
||||
if (!isset($headers[self::HEADER_ORIGIN]) || !$headers[self::HEADER_ORIGIN]) {
|
||||
throw new BadRequestException('No origin header');
|
||||
}
|
||||
|
||||
$origin = $headers[self::HEADER_ORIGIN];
|
||||
|
||||
if (!isset($headers[self::HEADER_UPGRADE])
|
||||
|| strtolower($headers[self::HEADER_UPGRADE]) != self::UPGRADE_VALUE
|
||||
) {
|
||||
throw new BadRequestException('Invalid upgrade header');
|
||||
}
|
||||
|
||||
if (!isset($headers[self::HEADER_CONNECTION])
|
||||
|| stripos($headers[self::HEADER_CONNECTION], self::CONNECTION_VALUE) === false
|
||||
) {
|
||||
throw new BadRequestException('Invalid connection header');
|
||||
}
|
||||
|
||||
if (!isset($headers[self::HEADER_HOST])) {
|
||||
// @todo Validate host == listening socket? Or would that break
|
||||
// TCP proxies?
|
||||
throw new BadRequestException('No host header');
|
||||
}
|
||||
|
||||
if (!isset($headers[self::HEADER_VERSION])) {
|
||||
throw new BadRequestException('No version header received on handshake request');
|
||||
}
|
||||
|
||||
if (!$this->acceptsVersion($headers[self::HEADER_VERSION])) {
|
||||
throw new BadRequestException('Unsupported version: ' . $version);
|
||||
}
|
||||
|
||||
if (!isset($headers[self::HEADER_KEY])) {
|
||||
throw new BadRequestException('No key header received');
|
||||
}
|
||||
|
||||
$key = trim($headers[self::HEADER_KEY]);
|
||||
|
||||
if (!$key) {
|
||||
throw new BadRequestException('Invalid key');
|
||||
}
|
||||
|
||||
// Optional
|
||||
$protocol = isset($headers[self::HEADER_PROTOCOL]) ? $headers[self::HEADER_PROTOCOL] : null;
|
||||
|
||||
$extensions = array();
|
||||
if (isset($headers[self::HEADER_EXTENSIONS]) && $headers[self::HEADER_EXTENSIONS]) {
|
||||
$extensions = $headers[self::HEADER_EXTENSIONS];
|
||||
if (is_scalar($extensions)) {
|
||||
$extensions = array($extensions);
|
||||
}
|
||||
}
|
||||
|
||||
return array($path, $origin, $key, $extensions, $protocol);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a suitable WebSocket close frame
|
||||
*
|
||||
* @param Exception|int $e
|
||||
*/
|
||||
public function getCloseFrame($e)
|
||||
{
|
||||
$code = false;
|
||||
|
||||
if ($e instanceof Exception) {
|
||||
$code = $e->getCode();
|
||||
} elseif (is_numeric($e)) {
|
||||
$code = (int)$e;
|
||||
}
|
||||
|
||||
if (!$code || !key_exists($code, self::$closeReasons)) {
|
||||
$code = self::CLOSE_UNEXPECTED;
|
||||
}
|
||||
|
||||
$body = pack('n', $code) . self::$closeReasons[$code];
|
||||
|
||||
$payload = $this->getPayload();
|
||||
return $payload->encode($body, self::TYPE_CLOSE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a WebSocket URI
|
||||
*
|
||||
* @param string $uri
|
||||
* @return array(string $scheme, string $host, int $port, string $path)
|
||||
*/
|
||||
public function validateUri($uri)
|
||||
{
|
||||
$uri = (string)$uri;
|
||||
if (!$uri) {
|
||||
throw new InvalidArgumentException('Invalid URI');
|
||||
}
|
||||
|
||||
$scheme = parse_url($uri, PHP_URL_SCHEME);
|
||||
$this->validateScheme($scheme);
|
||||
|
||||
$host = parse_url($uri, PHP_URL_HOST);
|
||||
if (!$host) {
|
||||
throw new InvalidArgumentException("Invalid host");
|
||||
}
|
||||
|
||||
$port = parse_url($uri, PHP_URL_PORT);
|
||||
if (!$port) {
|
||||
$port = $this->getPort($scheme);
|
||||
}
|
||||
|
||||
$path = parse_url($uri, PHP_URL_PATH);
|
||||
if (!$path) {
|
||||
throw new InvalidArgumentException('Invalid path');
|
||||
}
|
||||
|
||||
return array($scheme, $host, $port, $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a socket URI
|
||||
*
|
||||
* @param string $uri
|
||||
* @throws InvalidArgumentException
|
||||
* @return array(string $scheme, string $host, string $port)
|
||||
*/
|
||||
public function validateSocketUri($uri)
|
||||
{
|
||||
$uri = (string)$uri;
|
||||
if (!$uri) {
|
||||
throw new InvalidArgumentException('Invalid URI');
|
||||
}
|
||||
|
||||
$scheme = parse_url($uri, PHP_URL_SCHEME);
|
||||
$scheme = $this->validateScheme($scheme);
|
||||
|
||||
$host = parse_url($uri, PHP_URL_HOST);
|
||||
if (!$host) {
|
||||
throw new InvalidArgumentException("Invalid host");
|
||||
}
|
||||
|
||||
$port = parse_url($uri, PHP_URL_PORT);
|
||||
if (!$port) {
|
||||
$port = $this->getPort($scheme);
|
||||
}
|
||||
|
||||
return array($scheme, $host, $port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an origin URI
|
||||
*
|
||||
* @param string $origin
|
||||
* @throws InvalidArgumentException
|
||||
* @return string
|
||||
*/
|
||||
public function validateOriginUri($origin)
|
||||
{
|
||||
$origin = (string)$origin;
|
||||
if (!$origin) {
|
||||
throw new InvalidArgumentException('Invalid URI');
|
||||
}
|
||||
|
||||
$scheme = parse_url($origin, PHP_URL_SCHEME);
|
||||
if (!$scheme) {
|
||||
throw new InvalidArgumentException('Invalid scheme');
|
||||
}
|
||||
|
||||
$host = parse_url($origin, PHP_URL_HOST);
|
||||
if (!$host) {
|
||||
throw new InvalidArgumentException("Invalid host");
|
||||
}
|
||||
|
||||
return $origin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a request line
|
||||
*
|
||||
* @param string $line
|
||||
* @throws BadRequestException
|
||||
*/
|
||||
protected function validateRequestLine($line)
|
||||
{
|
||||
$matches = array(0 => null, 1 => null);
|
||||
|
||||
if (!preg_match(self::REQUEST_LINE_REGEX, $line, $matches) || !$matches[1]) {
|
||||
throw new BadRequestException('Invalid request line', 400);
|
||||
}
|
||||
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the expected accept value for a handshake response
|
||||
*
|
||||
* Note that the protocol calls for the base64 encoded value to be hashed,
|
||||
* not the original 16 byte random key.
|
||||
*
|
||||
* @see http://tools.ietf.org/html/rfc6455#section-4.2.2
|
||||
* @param string $key
|
||||
*/
|
||||
protected function getAcceptValue($encoded_key)
|
||||
{
|
||||
return base64_encode(sha1($encoded_key . self::MAGIC_GUID, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the headers from a full response
|
||||
*
|
||||
* @param string $response
|
||||
* @return array()
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function getHeaders($response, &$request_line = null)
|
||||
{
|
||||
$parts = explode("\r\n\r\n", $response, 2);
|
||||
|
||||
if (count($parts) != 2) {
|
||||
$parts = array($parts, '');
|
||||
}
|
||||
|
||||
list($headers, $body) = $parts;
|
||||
|
||||
$return = array();
|
||||
foreach (explode("\r\n", $headers) as $header) {
|
||||
$parts = explode(': ', $header, 2);
|
||||
if (count($parts) == 2) {
|
||||
list($name, $value) = $parts;
|
||||
if (!isset($return[$name])) {
|
||||
$return[$name] = $value;
|
||||
} else {
|
||||
if (is_array($return[$name])) {
|
||||
$return[$name][] = $value;
|
||||
} else {
|
||||
$return[$name] = array($return[$name], $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets request headers
|
||||
*
|
||||
* @param string $response
|
||||
* @return array<string, array<string>> The request line, and an array of
|
||||
* headers
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function getRequestHeaders($response)
|
||||
{
|
||||
$eol = stripos($response, "\r\n");
|
||||
|
||||
if ($eol === false) {
|
||||
throw new InvalidArgumentException('Invalid request line');
|
||||
}
|
||||
|
||||
$request = substr($response, 0, $eol);
|
||||
$headers = $this->getHeaders(substr($response, $eol + 2));
|
||||
|
||||
return array($request, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a scheme
|
||||
*
|
||||
* @param string $scheme
|
||||
* @return string Underlying scheme
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function validateScheme($scheme)
|
||||
{
|
||||
if (!$scheme) {
|
||||
throw new InvalidArgumentException('No scheme specified');
|
||||
}
|
||||
if (!in_array($scheme, self::$schemes)) {
|
||||
throw new InvalidArgumentException(
|
||||
'Unknown socket scheme: ' . $scheme
|
||||
);
|
||||
}
|
||||
|
||||
if ($scheme == self::SCHEME_WEBSOCKET_SECURE) {
|
||||
return self::SCHEME_UNDERLYING_SECURE;
|
||||
}
|
||||
return self::SCHEME_UNDERLYING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default request headers
|
||||
*
|
||||
* @param string $host
|
||||
* @param string $key
|
||||
* @param string $origin
|
||||
* @param int $version
|
||||
* @return multitype:unknown string NULL
|
||||
*/
|
||||
protected function getDefaultRequestHeaders($host, $key, $origin)
|
||||
{
|
||||
return array(
|
||||
self::HEADER_HOST => $host,
|
||||
self::HEADER_UPGRADE => self::UPGRADE_VALUE,
|
||||
self::HEADER_CONNECTION => self::CONNECTION_VALUE,
|
||||
self::HEADER_KEY => $key,
|
||||
self::HEADER_ORIGIN => $origin,
|
||||
self::HEADER_VERSION => $this->getVersion()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default response headers
|
||||
*
|
||||
* @param string $key
|
||||
*/
|
||||
protected function getSuccessResponseHeaders($key)
|
||||
{
|
||||
return array(
|
||||
self::HEADER_UPGRADE => self::UPGRADE_VALUE,
|
||||
self::HEADER_CONNECTION => self::CONNECTION_VALUE,
|
||||
self::HEADER_ACCEPT => $this->getAcceptValue($key)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default port for a scheme
|
||||
*
|
||||
* By default, the WebSocket Protocol uses port 80 for regular WebSocket
|
||||
* connections and port 443 for WebSocket connections tunneled over
|
||||
* Transport Layer Security
|
||||
*
|
||||
* @param string $uri
|
||||
* @return int
|
||||
*/
|
||||
protected function getPort($scheme)
|
||||
{
|
||||
if ($scheme == self::SCHEME_WEBSOCKET) {
|
||||
return 80;
|
||||
} elseif ($scheme == self::SCHEME_WEBSOCKET_SECURE) {
|
||||
return 443;
|
||||
} elseif ($scheme == self::SCHEME_UNDERLYING) {
|
||||
return 80;
|
||||
} elseif ($scheme == self::SCHEME_UNDERLYING_SECURE) {
|
||||
return 443;
|
||||
} else {
|
||||
throw new InvalidArgumentException('Unknown websocket scheme');
|
||||
}
|
||||
}
|
||||
}
|
36
listeners/lib/Wrench/Protocol/Rfc6455Protocol.php
Executable file
36
listeners/lib/Wrench/Protocol/Rfc6455Protocol.php
Executable file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Protocol;
|
||||
|
||||
use Wrench\Protocol\HybiProtocol;
|
||||
|
||||
/**
|
||||
* This is the version of websockets used by Chrome versions 17 through 19.
|
||||
*
|
||||
* @see http://tools.ietf.org/html/rfc6455
|
||||
*/
|
||||
class Rfc6455Protocol extends HybiProtocol
|
||||
{
|
||||
const VERSION = 13;
|
||||
|
||||
/**
|
||||
* @see Wrench\Protocol.Protocol::getVersion()
|
||||
*/
|
||||
public function getVersion()
|
||||
{
|
||||
return self::VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is our most recent protocol class
|
||||
*
|
||||
* @see Wrench\Protocol.Protocol::acceptsVersion()
|
||||
*/
|
||||
public function acceptsVersion($version)
|
||||
{
|
||||
if ((int)$version <= 13) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
12
listeners/lib/Wrench/Resource.php
Executable file
12
listeners/lib/Wrench/Resource.php
Executable file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench;
|
||||
|
||||
/**
|
||||
* Resource interface
|
||||
*/
|
||||
interface Resource
|
||||
{
|
||||
public function getResourceId();
|
||||
public function getResource();
|
||||
}
|
303
listeners/lib/Wrench/Server.php
Executable file
303
listeners/lib/Wrench/Server.php
Executable file
@ -0,0 +1,303 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench;
|
||||
|
||||
use Wrench\Util\Configurable;
|
||||
|
||||
use Wrench\Socket;
|
||||
use Wrench\Resource;
|
||||
|
||||
use \Closure;
|
||||
use \InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* WebSocket server
|
||||
*
|
||||
* The server extends socket, which provides the master socket resource. This
|
||||
* resource is listened to, and an array of clients managed.
|
||||
*
|
||||
* @author Nico Kaiser <nico@kaiser.me>
|
||||
* @author Simon Samtleben <web@lemmingzshadow.net>
|
||||
* @author Dominic Scheirlinck <dominic@varspool.com>
|
||||
*/
|
||||
class Server extends Configurable
|
||||
{
|
||||
/**#@+
|
||||
* Events
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const EVENT_SOCKET_CONNECT = 'socket_connect';
|
||||
const EVENT_SOCKET_DISCONNECT = 'socket_disconnect';
|
||||
const EVENT_HANDSHAKE_REQUEST = 'handshake_request';
|
||||
const EVENT_HANDSHAKE_SUCCESSFUL = 'handshake_successful';
|
||||
const EVENT_CLIENT_DATA = 'client_data';
|
||||
/**#@-*/
|
||||
|
||||
/**
|
||||
* The URI of the server
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $uri;
|
||||
|
||||
/**
|
||||
* Options
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $options = array();
|
||||
|
||||
/**
|
||||
* A logging callback
|
||||
*
|
||||
* The default callback simply prints to stdout. You can pass your own logger
|
||||
* in the options array. It should take a string message and string priority
|
||||
* as parameters.
|
||||
*
|
||||
* @var Closure
|
||||
*/
|
||||
protected $logger;
|
||||
|
||||
/**
|
||||
* Event listeners
|
||||
*
|
||||
* Add listeners using the addListener() method.
|
||||
*
|
||||
* @var array<string => array<Closure>>
|
||||
*/
|
||||
protected $listeners = array();
|
||||
|
||||
/**
|
||||
* Connection manager
|
||||
*
|
||||
* @var ConnectionManager
|
||||
*/
|
||||
protected $connectionManager;
|
||||
|
||||
/**
|
||||
* Applications
|
||||
*
|
||||
* @var array<string => Application>
|
||||
*/
|
||||
protected $applications = array();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $uri Websocket URI, e.g. ws://localhost:8000/, path will
|
||||
* be ignored
|
||||
* @param array $options (optional) See configure
|
||||
*/
|
||||
public function __construct($uri, array $options = array())
|
||||
{
|
||||
$this->uri = $uri;
|
||||
|
||||
parent::__construct($options);
|
||||
|
||||
$this->log('Server initialized', 'info');
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure options
|
||||
*
|
||||
* Options include
|
||||
* - socket_class => The socket class to use, defaults to ServerSocket
|
||||
* - socket_options => An array of socket options
|
||||
* - logger => Closure($message, $priority = 'info'), used
|
||||
* for logging
|
||||
*
|
||||
* @param array $options
|
||||
* @return void
|
||||
*/
|
||||
protected function configure(array $options)
|
||||
{
|
||||
$options = array_merge(array(
|
||||
'connection_manager_class' => 'Wrench\ConnectionManager',
|
||||
'connection_manager_options' => array()
|
||||
), $options);
|
||||
|
||||
parent::configure($options);
|
||||
|
||||
$this->configureConnectionManager();
|
||||
$this->configureLogger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the logger
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function configureLogger()
|
||||
{
|
||||
// Default logger
|
||||
if (!isset($this->options['logger'])) {
|
||||
$this->options['logger'] = function ($message, $priority = 'info') {
|
||||
printf("%s: %s%s", $priority, $message, PHP_EOL);
|
||||
};
|
||||
}
|
||||
$this->setLogger($this->options['logger']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the connection manager
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function configureConnectionManager()
|
||||
{
|
||||
$class = $this->options['connection_manager_class'];
|
||||
$options = $this->options['connection_manager_options'];
|
||||
$this->connectionManager = new $class($this, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the connection manager
|
||||
*
|
||||
* @return \Wrench\ConnectionManager
|
||||
*/
|
||||
public function getConnectionManager()
|
||||
{
|
||||
return $this->connectionManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getUri()
|
||||
{
|
||||
return $this->uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a logger
|
||||
*
|
||||
* @param Closure $logger
|
||||
* @return void
|
||||
*/
|
||||
public function setLogger($logger)
|
||||
{
|
||||
if (!is_callable($logger)) {
|
||||
throw new \InvalidArgumentException('Logger must be callable');
|
||||
}
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main server loop
|
||||
*
|
||||
* @return void This method does not return!
|
||||
*/
|
||||
public function run()
|
||||
{
|
||||
$this->connectionManager->listen();
|
||||
|
||||
while (true) {
|
||||
/*
|
||||
* If there's nothing changed on any of the sockets, the server
|
||||
* will sleep and other processes will have a change to run. Control
|
||||
* this behaviour with the timeout options.
|
||||
*/
|
||||
$this->connectionManager->selectAndProcess();
|
||||
|
||||
/*
|
||||
* If the application wants to perform periodic operations or queries and push updates to clients based on the result then that logic can be implemented in the 'onUpdate' method.
|
||||
*/
|
||||
foreach($this->applications as $application) {
|
||||
if(method_exists($application, 'onUpdate')) {
|
||||
$application->onUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message to the server log
|
||||
*
|
||||
* The default logger simply prints the message to stdout. You can provide
|
||||
* a logging closure. This is useful, for instance, if you've daemonized
|
||||
* and closed STDOUT.
|
||||
*
|
||||
* @param string $message Message to display.
|
||||
* @param string $type Type of message.
|
||||
* @return void
|
||||
*/
|
||||
public function log($message, $priority = 'info')
|
||||
{
|
||||
call_user_func($this->logger, $message, $priority);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies listeners of an event
|
||||
*
|
||||
* @param string $event
|
||||
* @param array $arguments Event arguments
|
||||
* @return void
|
||||
*/
|
||||
public function notify($event, array $arguments = array())
|
||||
{
|
||||
if (!isset($this->listeners[$event])) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->listeners[$event] as $listener) {
|
||||
call_user_func_array($listener, $arguments);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener
|
||||
*
|
||||
* Provide an event (see the Server::EVENT_* constants) and a callback
|
||||
* closure. Some arguments may be provided to your callback, such as the
|
||||
* connection the caused the event.
|
||||
*
|
||||
* @param string $event
|
||||
* @param Closure $callback
|
||||
* @return void
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function addListener($event, $callback)
|
||||
{
|
||||
if (!isset($this->listeners[$event])) {
|
||||
$this->listeners[$event] = array();
|
||||
}
|
||||
|
||||
if (!is_callable($callback)) {
|
||||
throw new InvalidArgumentException('Invalid listener');
|
||||
}
|
||||
|
||||
$this->listeners[$event][] = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a server application.
|
||||
*
|
||||
* @param string $key Name of application.
|
||||
* @return Application The application object.
|
||||
*/
|
||||
public function getApplication($key)
|
||||
{
|
||||
if (empty($key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (array_key_exists($key, $this->applications)) {
|
||||
return $this->applications[$key];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new application object to the application storage.
|
||||
*
|
||||
* @param string $key Name of application.
|
||||
* @param object $application The application object
|
||||
* @return void
|
||||
*/
|
||||
public function registerApplication($key, $application)
|
||||
{
|
||||
$this->applications[$key] = $application;
|
||||
}
|
||||
}
|
105
listeners/lib/Wrench/Socket/ClientSocket.php
Executable file
105
listeners/lib/Wrench/Socket/ClientSocket.php
Executable file
@ -0,0 +1,105 @@
|
||||
<?php
|
||||
namespace Wrench\Socket;
|
||||
|
||||
use Wrench\Socket\UriSocket;
|
||||
|
||||
/**
|
||||
* Options:
|
||||
* - timeout_connect => int, seconds, default 2
|
||||
*/
|
||||
class ClientSocket extends UriSocket
|
||||
{
|
||||
/**
|
||||
* Default connection timeout
|
||||
*
|
||||
* @var int seconds
|
||||
*/
|
||||
const TIMEOUT_CONNECT = 2;
|
||||
|
||||
/**
|
||||
* @see Wrench\Socket.Socket::configure()
|
||||
* Options include:
|
||||
* - ssl_verify_peer => boolean, whether to perform peer verification
|
||||
* of SSL certificate used
|
||||
* - ssl_allow_self_signed => boolean, whether ssl_verify_peer allows
|
||||
* self-signed certs
|
||||
* - timeout_connect => int, seconds, default 2
|
||||
*/
|
||||
protected function configure(array $options)
|
||||
{
|
||||
$options = array_merge(array(
|
||||
'timeout_connect' => self::TIMEOUT_CONNECT,
|
||||
'ssl_verify_peer' => false,
|
||||
'ssl_allow_self_signed' => true
|
||||
), $options);
|
||||
|
||||
parent::configure($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects to the given socket
|
||||
*/
|
||||
public function connect()
|
||||
{
|
||||
if ($this->isConnected()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$errno = null;
|
||||
$errstr = null;
|
||||
|
||||
$this->socket = stream_socket_client(
|
||||
$this->getUri(),
|
||||
$errno,
|
||||
$errstr,
|
||||
$this->options['timeout_connect'],
|
||||
STREAM_CLIENT_CONNECT,
|
||||
$this->getStreamContext()
|
||||
);
|
||||
|
||||
if (!$this->socket) {
|
||||
throw new ConnectionException(sprintf(
|
||||
'Could not connect to socket: %s (%d)',
|
||||
$errstr,
|
||||
$errno
|
||||
));
|
||||
}
|
||||
|
||||
stream_set_timeout($this->socket, $this->options['timeout_socket']);
|
||||
|
||||
return ($this->connected = true);
|
||||
}
|
||||
|
||||
public function reconnect()
|
||||
{
|
||||
$this->disconnect();
|
||||
$this->connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Wrench\Socket.UriSocket::getSocketStreamContextOptions()
|
||||
*/
|
||||
protected function getSocketStreamContextOptions()
|
||||
{
|
||||
$options = array();
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Wrench\Socket.UriSocket::getSslStreamContextOptions()
|
||||
*/
|
||||
protected function getSslStreamContextOptions()
|
||||
{
|
||||
$options = array();
|
||||
|
||||
if ($this->options['ssl_verify_peer']) {
|
||||
$options['verify_peer'] = true;
|
||||
}
|
||||
|
||||
if ($this->options['ssl_allow_self_signed']) {
|
||||
$options['allow_self_signed'] = true;
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
}
|
25
listeners/lib/Wrench/Socket/ServerClientSocket.php
Executable file
25
listeners/lib/Wrench/Socket/ServerClientSocket.php
Executable file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Socket;
|
||||
|
||||
use Wrench\Socket\Socket;
|
||||
|
||||
class ServerClientSocket extends Socket
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* A server client socket is accepted from a listening socket, so there's
|
||||
* no need to call ->connect() or whatnot.
|
||||
*
|
||||
* @param resource $accepted_socket
|
||||
* @param array $options
|
||||
*/
|
||||
public function __construct($accepted_socket, array $options = array())
|
||||
{
|
||||
parent::__construct($options);
|
||||
|
||||
$this->socket = $accepted_socket;
|
||||
$this->connected = (boolean)$accepted_socket;
|
||||
}
|
||||
}
|
126
listeners/lib/Wrench/Socket/ServerSocket.php
Executable file
126
listeners/lib/Wrench/Socket/ServerSocket.php
Executable file
@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Socket;
|
||||
|
||||
use Wrench\Exception\ConnectionException;
|
||||
|
||||
use Wrench\Socket\UriSocket;
|
||||
|
||||
/**
|
||||
* Server socket
|
||||
*
|
||||
* Used for a server's "master" socket that binds to the configured
|
||||
* interface and listens
|
||||
*/
|
||||
class ServerSocket extends UriSocket
|
||||
{
|
||||
const TIMEOUT_ACCEPT = 5;
|
||||
|
||||
/**
|
||||
* Whether the socket is listening
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $listening = false;
|
||||
|
||||
/**
|
||||
* @see Wrench\Socket.Socket::configure()
|
||||
* Options include:
|
||||
* - backlog => int, used to limit the number of outstanding
|
||||
* connections in the socket's listen queue
|
||||
* - ssl_cert_file => string, server SSL certificate
|
||||
* file location. File should contain
|
||||
* certificate and private key
|
||||
* - ssl_passphrase => string, passphrase for the key
|
||||
* - timeout_accept => int, seconds, default 5
|
||||
*/
|
||||
protected function configure(array $options)
|
||||
{
|
||||
$options = array_merge(array(
|
||||
'backlog' => 50,
|
||||
'ssl_cert_file' => null,
|
||||
'ssl_passphrase' => null,
|
||||
'ssl_allow_self_signed' => false,
|
||||
'timeout_accept' => self::TIMEOUT_ACCEPT
|
||||
), $options);
|
||||
|
||||
parent::configure($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens
|
||||
*
|
||||
* @throws ConnectionException
|
||||
*/
|
||||
public function listen()
|
||||
{
|
||||
$this->socket = stream_socket_server(
|
||||
$this->getUri(),
|
||||
$errno,
|
||||
$errstr,
|
||||
STREAM_SERVER_BIND|STREAM_SERVER_LISTEN.
|
||||
$this->getStreamContext()
|
||||
);
|
||||
|
||||
if (!$this->socket) {
|
||||
throw new ConnectionException(sprintf(
|
||||
'Could not listen on socket: %s (%d)',
|
||||
$errstr,
|
||||
$errno
|
||||
));
|
||||
}
|
||||
|
||||
$this->listening = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts a new connection on the socket
|
||||
*
|
||||
* @throws ConnectionException
|
||||
* @return resource
|
||||
*/
|
||||
public function accept()
|
||||
{
|
||||
$new = stream_socket_accept(
|
||||
$this->socket,
|
||||
$this->options['timeout_accept']
|
||||
);
|
||||
|
||||
if (!$new) {
|
||||
throw new ConnectionException(socket_strerror(socket_last_error($new)));
|
||||
}
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Wrench\Socket.UriSocket::getSocketStreamContextOptions()
|
||||
*/
|
||||
protected function getSocketStreamContextOptions()
|
||||
{
|
||||
$options = array();
|
||||
|
||||
if ($this->options['backlog']) {
|
||||
$options['backlog'] = $this->options['backlog'];
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Wrench\Socket.UriSocket::getSslStreamContextOptions()
|
||||
*/
|
||||
protected function getSslStreamContextOptions()
|
||||
{
|
||||
$options = array();
|
||||
|
||||
if ($this->options['server_ssl_cert_file']) {
|
||||
$options['local_cert'] = $this->options['server_ssl_cert_file'];
|
||||
if ($this->options['server_ssl_passphrase']) {
|
||||
$options['passphrase'] = $this->options['server_ssl_passphrase'];
|
||||
}
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
}
|
322
listeners/lib/Wrench/Socket/Socket.php
Executable file
322
listeners/lib/Wrench/Socket/Socket.php
Executable file
@ -0,0 +1,322 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Socket;
|
||||
|
||||
use Wrench\Resource;
|
||||
use Wrench\Exception\ConnectionException;
|
||||
use Wrench\Exception\SocketException;
|
||||
use Wrench\Util\Configurable;
|
||||
use Wrench\Protocol\Protocol;
|
||||
use Wrench\Protocol\Rfc6455Protocol;
|
||||
use \InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Socket class
|
||||
*
|
||||
* Implements low level logic for connecting, serving, reading to, and
|
||||
* writing from WebSocket connections using PHP's streams.
|
||||
*
|
||||
* Unlike in previous versions of this library, a Socket instance now
|
||||
* represents a single underlying socket resource. It's designed to be used
|
||||
* by aggregation, rather than inheritence.
|
||||
*/
|
||||
abstract class Socket extends Configurable implements Resource
|
||||
{
|
||||
/**
|
||||
* Default timeout for socket operations (reads, writes)
|
||||
*
|
||||
* @var int seconds
|
||||
*/
|
||||
const TIMEOUT_SOCKET = 5;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
const DEFAULT_RECEIVE_LENGTH = '1400';
|
||||
|
||||
/**#@+
|
||||
* Socket name parts
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const NAME_PART_IP = 0;
|
||||
const NAME_PART_PORT = 1;
|
||||
/**#@-*/
|
||||
|
||||
/**
|
||||
* @var resource
|
||||
*/
|
||||
protected $socket = null;
|
||||
|
||||
/**
|
||||
* Stream context
|
||||
*/
|
||||
protected $context = null;
|
||||
|
||||
/**
|
||||
* Whether the socket is connected to a server
|
||||
*
|
||||
* Note, the connection may not be ready to use, but the socket is
|
||||
* connected at least. See $handshaked, and other properties in
|
||||
* subclasses.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $connected = false;
|
||||
|
||||
/**
|
||||
* Whether the current read is the first one to the socket
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $firstRead = true;
|
||||
|
||||
/**
|
||||
* The socket name according to stream_socket_get_name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* Configure options
|
||||
*
|
||||
* Options include
|
||||
* - timeout_connect => int, seconds, default 2
|
||||
* - timeout_socket => int, seconds, default 5
|
||||
*
|
||||
* @param array $options
|
||||
* @return void
|
||||
*/
|
||||
protected function configure(array $options)
|
||||
{
|
||||
$options = array_merge(array(
|
||||
'timeout_socket' => self::TIMEOUT_SOCKET,
|
||||
), $options);
|
||||
|
||||
parent::configure($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of the socket
|
||||
*/
|
||||
protected function getName()
|
||||
{
|
||||
if (!isset($this->name) || !$this->name) {
|
||||
$this->name = @stream_socket_get_name($this->socket, true);
|
||||
}
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets part of the name of the socket
|
||||
*
|
||||
* PHP seems to return IPV6 address/port combos like this:
|
||||
* ::1:1234, where ::1 is the address and 1234 the port
|
||||
* So, the part number here is either the last : delimited section (the port)
|
||||
* or all the other sections (the whole initial part, the address).
|
||||
*
|
||||
* @param string $name (from $this->getName() usually)
|
||||
* @param int<0, 1> $part
|
||||
* @return string
|
||||
* @throws SocketException
|
||||
*/
|
||||
public static function getNamePart($name, $part)
|
||||
{
|
||||
if (!$name) {
|
||||
throw new InvalidArgumentException('Invalid name');
|
||||
}
|
||||
|
||||
$parts = explode(':', $name);
|
||||
|
||||
if (count($parts) < 2) {
|
||||
throw new SocketException('Could not parse name parts: ' . $name);
|
||||
}
|
||||
|
||||
if ($part == self::NAME_PART_PORT) {
|
||||
return end($parts);
|
||||
} elseif ($part == self::NAME_PART_IP) {
|
||||
return implode(':', array_slice($parts, 0, -1));
|
||||
} else {
|
||||
throw new InvalidArgumentException('Invalid name part');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the IP address of the socket
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getIp()
|
||||
{
|
||||
$name = $this->getName();
|
||||
|
||||
if ($name) {
|
||||
return self::getNamePart($name, self::NAME_PART_IP);
|
||||
} else {
|
||||
throw new SocketException('Cannot get socket IP address');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the port of the socket
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getPort()
|
||||
{
|
||||
$name = $this->getName();
|
||||
|
||||
if ($name) {
|
||||
return self::getNamePart($name, self::NAME_PART_PORT);
|
||||
} else {
|
||||
throw new SocketException('Cannot get socket IP address');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last error that occurred on the socket
|
||||
*
|
||||
* @return int|string
|
||||
*/
|
||||
public function getLastError()
|
||||
{
|
||||
if ($this->isConnected() && $this->socket) {
|
||||
$err = @socket_last_error($this->socket);
|
||||
if ($err) {
|
||||
$err = socket_strerror($err);
|
||||
}
|
||||
if (!$err) {
|
||||
$err = 'Unknown error';
|
||||
}
|
||||
return $err;
|
||||
} else {
|
||||
return 'Not connected';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the socket is currently connected
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isConnected()
|
||||
{
|
||||
return $this->connected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect the socket
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function disconnect()
|
||||
{
|
||||
if ($this->socket) {
|
||||
stream_socket_shutdown($this->socket, STREAM_SHUT_RDWR);
|
||||
}
|
||||
$this->socket = null;
|
||||
$this->connected = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Wrench.Resource::getResource()
|
||||
*/
|
||||
public function getResource()
|
||||
{
|
||||
return $this->socket;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Wrench.Resource::getResourceId()
|
||||
*/
|
||||
public function getResourceId()
|
||||
{
|
||||
return (int)$this->socket;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param unknown_type $data
|
||||
* @throws SocketException
|
||||
* @return boolean|int The number of bytes sent or false on error
|
||||
*/
|
||||
public function send($data)
|
||||
{
|
||||
if (!$this->isConnected()) {
|
||||
throw new SocketException('Socket is not connected');
|
||||
}
|
||||
|
||||
$length = strlen($data);
|
||||
|
||||
if ($length == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for ($i = $length; $i > 0; $i -= $written) {
|
||||
$written = @fwrite($this->socket, substr($data, -1 * $i));
|
||||
|
||||
if ($written === false) {
|
||||
return false;
|
||||
} elseif ($written === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return $length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recieve data from the socket
|
||||
*
|
||||
* @param int $length
|
||||
* @return string
|
||||
*/
|
||||
public function receive($length = self::DEFAULT_RECEIVE_LENGTH)
|
||||
{
|
||||
$remaining = $length;
|
||||
|
||||
$buffer = '';
|
||||
$metadata['unread_bytes'] = 0;
|
||||
|
||||
do {
|
||||
if (feof($this->socket)) {
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
$result = fread($this->socket, $length);
|
||||
|
||||
if ($result === false) {
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
$buffer .= $result;
|
||||
|
||||
if (feof($this->socket)) {
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
$continue = false;
|
||||
|
||||
if ($this->firstRead == true && strlen($result) == 1) {
|
||||
// Workaround Chrome behavior (still needed?)
|
||||
$continue = true;
|
||||
}
|
||||
$this->firstRead = false;
|
||||
|
||||
if (strlen($result) == $length) {
|
||||
$continue = true;
|
||||
}
|
||||
|
||||
// Continue if more data to be read
|
||||
$metadata = stream_get_meta_data($this->socket);
|
||||
if ($metadata && isset($metadata['unread_bytes']) && $metadata['unread_bytes']) {
|
||||
$continue = true;
|
||||
$length = $metadata['unread_bytes'];
|
||||
}
|
||||
} while ($continue);
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
}
|
118
listeners/lib/Wrench/Socket/UriSocket.php
Executable file
118
listeners/lib/Wrench/Socket/UriSocket.php
Executable file
@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Socket;
|
||||
|
||||
use Wrench\Protocol\Protocol;
|
||||
|
||||
use Wrench\Socket\Socket;
|
||||
|
||||
abstract class UriSocket extends Socket
|
||||
{
|
||||
protected $scheme;
|
||||
protected $host;
|
||||
protected $port;
|
||||
|
||||
/**
|
||||
* URI Socket constructor
|
||||
*
|
||||
* @param string $uri WebSocket URI, e.g. ws://example.org:8000/chat
|
||||
* @param array $options (optional)
|
||||
* Options:
|
||||
* - protocol => Wrench\Protocol object, latest protocol
|
||||
* version used if not specified
|
||||
* - timeout_socket => int, seconds, default 5
|
||||
* - server_ssl_cert_file => string, server SSL certificate
|
||||
* file location. File should contain
|
||||
* certificate and private key
|
||||
* - server_ssl_passphrase => string, passphrase for the key
|
||||
* - server_ssl_allow_self_signed => boolean, whether to allows self-
|
||||
* signed certs
|
||||
*/
|
||||
public function __construct($uri, array $options = array())
|
||||
{
|
||||
parent::__construct($options);
|
||||
|
||||
list($this->scheme, $this->host, $this->port)
|
||||
= $this->protocol->validateSocketUri($uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the canonical/normalized URI for this socket
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getUri()
|
||||
{
|
||||
return sprintf(
|
||||
'%s://%s:%d',
|
||||
$this->scheme,
|
||||
$this->host,
|
||||
$this->port
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo DNS lookup? Override getIp()?
|
||||
* @see Wrench\Socket.Socket::getName()
|
||||
*/
|
||||
protected function getName()
|
||||
{
|
||||
return sprintf('%s:%s', $this->host, $this->port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the host name
|
||||
*/
|
||||
public function getHost()
|
||||
{
|
||||
return $this->host;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Wrench\Socket.Socket::getPort()
|
||||
*/
|
||||
public function getPort()
|
||||
{
|
||||
return $this->port;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a stream context
|
||||
*/
|
||||
protected function getStreamContext($listen = false)
|
||||
{
|
||||
$options = array();
|
||||
|
||||
if ($this->scheme == Protocol::SCHEME_UNDERLYING_SECURE
|
||||
|| $this->scheme == Protocol::SCHEME_UNDERLYING) {
|
||||
$options['socket'] = $this->getSocketStreamContextOptions();
|
||||
}
|
||||
|
||||
if ($this->scheme == Protocol::SCHEME_UNDERLYING_SECURE) {
|
||||
$options['ssl'] = $this->getSslStreamContextOptions();
|
||||
}
|
||||
|
||||
return stream_context_create(
|
||||
$options,
|
||||
array()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of socket stream context options
|
||||
*
|
||||
* See http://php.net/manual/en/context.socket.php
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
abstract protected function getSocketStreamContextOptions();
|
||||
|
||||
/**
|
||||
* Returns an array of ssl stream context options
|
||||
*
|
||||
* See http://php.net/manual/en/context.ssl.php
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
abstract protected function getSslStreamContextOptions();
|
||||
}
|
57
listeners/lib/Wrench/Tests/Application/EchoApplicationTest.php
Executable file
57
listeners/lib/Wrench/Tests/Application/EchoApplicationTest.php
Executable file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Tests\Application;
|
||||
|
||||
use Wrench\Protocol\Protocol;
|
||||
use Wrench\Tests\Test as WrenchTest;
|
||||
|
||||
class EchoApplicationTest extends WrenchTest
|
||||
{
|
||||
/**
|
||||
* @see Wrench\Tests.Test::getClass()
|
||||
*/
|
||||
protected function getClass()
|
||||
{
|
||||
return 'Wrench\Application\EchoApplication';
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the constructor
|
||||
*/
|
||||
public function testConstructor()
|
||||
{
|
||||
$this->assertInstanceOfClass($this->getInstance());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param unknown_type $payload
|
||||
* @dataProvider getValidPayloads
|
||||
*/
|
||||
public function testOnData($payload)
|
||||
{
|
||||
$connection = $this->getMockBuilder('Wrench\Connection')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
|
||||
$connection
|
||||
->expects($this->once())
|
||||
->method('send')
|
||||
->with($this->equalTo($payload), $this->equalTo(Protocol::TYPE_TEXT))
|
||||
->will($this->returnValue(true));
|
||||
|
||||
$this->getInstance()->onData($payload, $connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider
|
||||
*
|
||||
* @return array<array<string>>
|
||||
*/
|
||||
public function getValidPayloads()
|
||||
{
|
||||
return array(
|
||||
array('asdkllakdaowidoaw noaoinosdna nwodinado ndsnd aklndiownd'),
|
||||
array(' ')
|
||||
);
|
||||
}
|
||||
}
|
119
listeners/lib/Wrench/Tests/BasicServerTest.php
Executable file
119
listeners/lib/Wrench/Tests/BasicServerTest.php
Executable file
@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Tests;
|
||||
|
||||
use Wrench\Server;
|
||||
use Wrench\BasicServer;
|
||||
use Wrench\Tests\ServerTest;
|
||||
use Wrench\Socket;
|
||||
|
||||
use \InvalidArgumentException;
|
||||
use \PHPUnit_Framework_Error;
|
||||
|
||||
/**
|
||||
* Tests the BasicServer class
|
||||
*/
|
||||
class BasicServerTest extends ServerTest
|
||||
{
|
||||
/**
|
||||
* @see Wrench\Tests.Test::getClass()
|
||||
*/
|
||||
protected function getClass()
|
||||
{
|
||||
return 'Wrench\BasicServer';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $allowed
|
||||
* @param string $origin
|
||||
* @dataProvider getValidOrigins
|
||||
*/
|
||||
public function testValidOriginPolicy(array $allowed, $origin)
|
||||
{
|
||||
$server = $this->getInstance('ws://localhost:8000', array(
|
||||
'allowed_origins' => $allowed,
|
||||
'logger' => array($this, 'log')
|
||||
));
|
||||
|
||||
$connection = $this->getMockBuilder('Wrench\Connection')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
|
||||
$connection
|
||||
->expects($this->never())
|
||||
->method('close')
|
||||
->will($this->returnValue(true));
|
||||
|
||||
$server->notify(
|
||||
Server::EVENT_HANDSHAKE_REQUEST,
|
||||
array($connection, '', $origin, '', array())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $allowed
|
||||
* @param string $origin
|
||||
* @dataProvider getInvalidOrigins
|
||||
*/
|
||||
public function testInvalidOriginPolicy(array $allowed, $origin)
|
||||
{
|
||||
$server = $this->getInstance('ws://localhost:8000', array(
|
||||
'allowed_origins' => $allowed,
|
||||
'logger' => array($this, 'log')
|
||||
));
|
||||
|
||||
$connection = $this->getMockBuilder('Wrench\Connection')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
|
||||
$connection
|
||||
->expects($this->once())
|
||||
->method('close')
|
||||
->will($this->returnValue(true));
|
||||
|
||||
$server->notify(
|
||||
Server::EVENT_HANDSHAKE_REQUEST,
|
||||
array($connection, '', $origin, '', array())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Wrench\Tests.ServerTest::getValidConstructorArguments()
|
||||
*/
|
||||
public function getValidConstructorArguments()
|
||||
{
|
||||
return array_merge(parent::getValidConstructorArguments(), array(
|
||||
array(
|
||||
'ws://localhost:8000',
|
||||
array('logger' => function () {})
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider
|
||||
*
|
||||
* @return array<array<mixed>>
|
||||
*/
|
||||
public function getValidOrigins()
|
||||
{
|
||||
return array(
|
||||
array(array('localhost'), 'localhost'),
|
||||
array(array('somewhere.com'), 'somewhere.com'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider
|
||||
*
|
||||
* @return array<array<mixed>>
|
||||
*/
|
||||
public function getInvalidOrigins()
|
||||
{
|
||||
return array(
|
||||
array(array('localhost'), 'blah'),
|
||||
array(array('somewhere.com'), 'somewhereelse.com'),
|
||||
array(array('somewhere.com'), 'subdomain.somewhere.com')
|
||||
);
|
||||
}
|
||||
}
|
173
listeners/lib/Wrench/Tests/ClientTest.php
Executable file
173
listeners/lib/Wrench/Tests/ClientTest.php
Executable file
@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Tests;
|
||||
|
||||
use Wrench\Protocol\Protocol;
|
||||
|
||||
use Wrench\Client;
|
||||
use Wrench\Tests\Test;
|
||||
use Wrench\Socket;
|
||||
|
||||
use \InvalidArgumentException;
|
||||
use \PHPUnit_Framework_Error;
|
||||
|
||||
/**
|
||||
* Tests the client class
|
||||
*/
|
||||
class ClientTest extends Test
|
||||
{
|
||||
/**
|
||||
* @see Wrench\Tests.Test::getClass()
|
||||
*/
|
||||
protected function getClass()
|
||||
{
|
||||
return 'Wrench\Client';
|
||||
}
|
||||
|
||||
public function testConstructor()
|
||||
{
|
||||
$this->assertInstanceOfClass(
|
||||
$client = new Client(
|
||||
'ws://localhost/test', 'http://example.org/'
|
||||
),
|
||||
'ws:// scheme, default socket'
|
||||
);
|
||||
|
||||
$this->assertInstanceOfClass(
|
||||
$client = new Client(
|
||||
'ws://localhost/test', 'http://example.org/',
|
||||
array('socket' => $this->getMockSocket())
|
||||
),
|
||||
'ws:// scheme, socket specified'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a mock socket
|
||||
*
|
||||
* @return Socket
|
||||
*/
|
||||
protected function getMockSocket()
|
||||
{
|
||||
return $this->getMock('Wrench\Socket\ClientSocket', array(), array('wss://localhost:8000'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException PHPUnit_Framework_Error
|
||||
*/
|
||||
public function testConstructorSocketUnspecified()
|
||||
{
|
||||
$w = new Client();
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException InvalidArgumentException
|
||||
*/
|
||||
public function testConstructorUriInvalid()
|
||||
{
|
||||
$w = new Client('invalid uri', 'http://www.example.com/');
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException InvalidArgumentException
|
||||
*/
|
||||
public function testConstructorUriEmpty()
|
||||
{
|
||||
$w = new Client(null, 'http://www.example.com/');
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException InvalidArgumentException
|
||||
*/
|
||||
public function testConstructorUriPathUnspecified()
|
||||
{
|
||||
$w = new Client('ws://localhost', 'http://www.example.com/');
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException PHPUnit_Framework_Error
|
||||
*/
|
||||
public function testConstructorOriginUnspecified()
|
||||
{
|
||||
$w = new Client('ws://localhost');
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException InvalidArgumentException
|
||||
*/
|
||||
public function testConstructorOriginEmpty()
|
||||
{
|
||||
$w = new Client('wss://localhost', null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException InvalidArgumentException
|
||||
*/
|
||||
public function testConstructorOriginInvalid()
|
||||
{
|
||||
$w = new Client('ws://localhost:8000', 'NOTAVALIDURI');
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException InvalidArgumentException
|
||||
*/
|
||||
public function testSendInvalidType()
|
||||
{
|
||||
$client = new Client('ws://localhost/test', 'http://example.org/');
|
||||
$client->sendData('blah', 9999);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException InvalidArgumentException
|
||||
*/
|
||||
public function testSendInvalidTypeString()
|
||||
{
|
||||
$client = new Client('ws://localhost/test', 'http://example.org/');
|
||||
$client->sendData('blah', 'fooey');
|
||||
}
|
||||
|
||||
public function testSend()
|
||||
{
|
||||
try {
|
||||
$helper = new ServerTestHelper();
|
||||
$helper->setUp();
|
||||
|
||||
/* @var $instance Wrench\Client */
|
||||
$instance = $this->getInstance($helper->getEchoConnectionString(), 'http://www.example.com/send');
|
||||
$instance->addRequestHeader('X-Test', 'Custom Request Header');
|
||||
|
||||
$this->assertFalse($instance->receive(), 'Receive before connect');
|
||||
|
||||
$success = $instance->connect();
|
||||
$this->assertTrue($success, 'Client can connect to test server');
|
||||
$this->assertTrue($instance->isConnected());
|
||||
|
||||
$this->assertFalse($instance->connect(), 'Double connect');
|
||||
|
||||
$this->assertFalse((boolean)$instance->receive(), 'No data');
|
||||
|
||||
$bytes = $instance->sendData('foobar', 'text');
|
||||
$this->assertTrue($bytes >= 6, 'sent text frame');
|
||||
sleep(1);
|
||||
|
||||
$bytes = $instance->sendData('baz', Protocol::TYPE_TEXT);
|
||||
$this->assertTrue($bytes >= 3, 'sent text frame');
|
||||
sleep(1);
|
||||
|
||||
$responses = $instance->receive();
|
||||
$this->assertTrue(is_array($responses));
|
||||
$this->assertCount(2, $responses);
|
||||
$this->assertInstanceOf('Wrench\\Payload\\Payload', $responses[0]);
|
||||
$this->assertInstanceOf('Wrench\\Payload\\Payload', $responses[1]);
|
||||
|
||||
$instance->disconnect();
|
||||
|
||||
$this->assertFalse($instance->isConnected());
|
||||
} catch (\Exception $e) {
|
||||
$helper->tearDown();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$helper->tearDown();
|
||||
}
|
||||
}
|
101
listeners/lib/Wrench/Tests/ConnectionManagerTest.php
Executable file
101
listeners/lib/Wrench/Tests/ConnectionManagerTest.php
Executable file
@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Tests;
|
||||
|
||||
use Wrench\ConnectionManager;
|
||||
use Wrench\Tests\Test;
|
||||
|
||||
use Wrench\Application\EchoApplication;
|
||||
|
||||
use \InvalidArgumentException;
|
||||
use \PHPUnit_Framework_Error;
|
||||
|
||||
/**
|
||||
* Tests the ConnectionManager class
|
||||
*/
|
||||
class ConnectionManagerTest extends Test
|
||||
{
|
||||
/**
|
||||
* @see Wrench\Tests.Test::getClass()
|
||||
*/
|
||||
protected function getClass()
|
||||
{
|
||||
return 'Wrench\ConnectionManager';
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the constructor
|
||||
*
|
||||
* @dataProvider getValidConstructorArguments
|
||||
*/
|
||||
public function testValidConstructorArguments($server, array $options)
|
||||
{
|
||||
$this->assertInstanceOfClass(
|
||||
$instance = $this->getInstance(
|
||||
$server,
|
||||
$options
|
||||
),
|
||||
'Valid constructor arguments'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the constructor
|
||||
*/
|
||||
public function testConstructor()
|
||||
{
|
||||
$this->assertInstanceOfClass(
|
||||
$instance = $this->getInstance(
|
||||
$this->getMockServer(),
|
||||
array()
|
||||
),
|
||||
'Constructor'
|
||||
);
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testConstructor
|
||||
* @param ConnectionManager $instance
|
||||
*/
|
||||
public function testCount($instance)
|
||||
{
|
||||
$this->assertTrue(is_numeric($instance->count()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider
|
||||
*/
|
||||
public function getValidConstructorArguments()
|
||||
{
|
||||
return array(
|
||||
array($this->getMockServer(), array())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a mock server
|
||||
*/
|
||||
protected function getMockServer()
|
||||
{
|
||||
$server = $this->getMock('Wrench\Server', array(), array(), '', false);
|
||||
|
||||
$server->registerApplication('/echo', $this->getMockApplication());
|
||||
|
||||
$server->expects($this->any())
|
||||
->method('getUri')
|
||||
->will($this->returnValue('ws://localhost:8000/'));
|
||||
|
||||
return $server;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a mock application
|
||||
*
|
||||
* @return EchoApplication
|
||||
*/
|
||||
protected function getMockApplication()
|
||||
{
|
||||
return new EchoApplication();
|
||||
}
|
||||
}
|
386
listeners/lib/Wrench/Tests/ConnectionTest.php
Executable file
386
listeners/lib/Wrench/Tests/ConnectionTest.php
Executable file
@ -0,0 +1,386 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Tests;
|
||||
|
||||
use Wrench\Application\EchoApplication;
|
||||
|
||||
use Wrench\Protocol\Protocol;
|
||||
|
||||
use Wrench\Connection;
|
||||
use Wrench\Tests\Test;
|
||||
use Wrench\Socket;
|
||||
|
||||
use \InvalidArgumentException;
|
||||
use \PHPUnit_Framework_Error;
|
||||
|
||||
/**
|
||||
* Tests the Connection class
|
||||
*/
|
||||
class ConnectionTest extends Test
|
||||
{
|
||||
/**
|
||||
* @see Wrench\Tests.Test::getClass()
|
||||
*/
|
||||
protected function getClass()
|
||||
{
|
||||
return 'Wrench\Connection';
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the constructor
|
||||
*
|
||||
* @dataProvider getValidConstructorArguments
|
||||
*/
|
||||
public function testConstructor($manager, $socket, array $options)
|
||||
{
|
||||
$this->assertInstanceOfClass(
|
||||
$instance = $this->getInstance(
|
||||
$manager,
|
||||
$socket,
|
||||
$options
|
||||
),
|
||||
'Valid constructor arguments'
|
||||
);
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getValidCloseCodes
|
||||
*/
|
||||
public function testClose($code)
|
||||
{
|
||||
$socket = $this->getMockSocket();
|
||||
|
||||
$socket->expects($this->any())
|
||||
->method('getIp')
|
||||
->will($this->returnValue('127.0.0.1'));
|
||||
|
||||
$socket->expects($this->any())
|
||||
->method('getPort')
|
||||
->will($this->returnValue(mt_rand(1025, 50000)));
|
||||
|
||||
$manager = $this->getMockConnectionManager();
|
||||
|
||||
$connection = $this->getInstance($manager, $socket);
|
||||
$connection->close($code);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getValidHandshakeData
|
||||
*/
|
||||
public function testHandshake($path, $request)
|
||||
{
|
||||
$connection = $this->getConnectionForHandshake(
|
||||
$this->getConnectedSocket(),
|
||||
$path,
|
||||
$request
|
||||
);
|
||||
$connection->handshake($request);
|
||||
$connection->onData('somedata');
|
||||
$this->assertTrue($connection->send('someotherdata'));
|
||||
return $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getValidHandshakeData
|
||||
* @expectedException Wrench\Exception\HandshakeException
|
||||
*/
|
||||
public function testHandshakeBadSocket($path, $request)
|
||||
{
|
||||
$connection = $this->getConnectionForHandshake(
|
||||
$this->getNotConnectedSocket(),
|
||||
$path,
|
||||
$request
|
||||
);
|
||||
$connection->handshake($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Because expectation is that only $path application is available
|
||||
*
|
||||
* @dataProvider getWrongPathHandshakeData
|
||||
* @expectedException PHPUnit_Framework_ExpectationFailedException
|
||||
*/
|
||||
public function testWrongPathHandshake($path, $request)
|
||||
{
|
||||
$connection = $this->getConnectionForHandshake(
|
||||
$this->getConnectedSocket(),
|
||||
$path,
|
||||
$request
|
||||
);
|
||||
$connection->handshake($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getValidHandleData
|
||||
*/
|
||||
public function testHandle($path, $request_handshake, array $requests, array $counts)
|
||||
{
|
||||
$connection = $this->getConnectionForHandle(
|
||||
$this->getConnectedSocket(),
|
||||
$path,
|
||||
$request_handshake,
|
||||
$counts
|
||||
);
|
||||
|
||||
$connection->handshake($request_handshake);
|
||||
|
||||
foreach ($requests as $request) {
|
||||
$connection->handle($request);
|
||||
}
|
||||
|
||||
return $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Socket
|
||||
*/
|
||||
protected function getConnectedSocket()
|
||||
{
|
||||
$socket = $this->getMockSocket();
|
||||
|
||||
$socket->expects($this->any())
|
||||
->method('isConnected')
|
||||
->will($this->returnValue(true));
|
||||
|
||||
return $socket;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Socket
|
||||
*/
|
||||
protected function getNotConnectedSocket()
|
||||
{
|
||||
$socket = $this->getMockSocket();
|
||||
|
||||
$socket->expects($this->any())
|
||||
->method('isConnected')
|
||||
->will($this->returnValue(false));
|
||||
|
||||
return $socket;
|
||||
}
|
||||
|
||||
protected function getConnectionForHandshake($socket, $path, $request)
|
||||
{
|
||||
$manager = $this->getMockConnectionManager();
|
||||
|
||||
$application = $this->getMockApplication();
|
||||
|
||||
$server = $this->getMock('Wrench\Server', array(), array(), '', false);
|
||||
$server->registerApplication($path, $application);
|
||||
|
||||
$manager->expects($this->any())
|
||||
->method('getApplicationForPath')
|
||||
->with($path)
|
||||
->will($this->returnValue($application));
|
||||
|
||||
$manager->expects($this->any())
|
||||
->method('getServer')
|
||||
->will($this->returnValue($server));
|
||||
|
||||
$connection = $this->getInstance($manager, $socket);
|
||||
|
||||
return $connection;
|
||||
}
|
||||
|
||||
protected function getConnectionForHandle($socket, $path, $handshake, array $counts)
|
||||
{
|
||||
$connection = $this->getConnectionForHandshake($socket, $path, $handshake);
|
||||
|
||||
$manager = $this->getMockConnectionManager();
|
||||
|
||||
$application = $this->getMockApplication();
|
||||
|
||||
$application->expects($this->exactly(isset($counts['onData']) ? $counts['onData'] : 0))
|
||||
->method('onData')
|
||||
->will($this->returnValue(true));
|
||||
|
||||
$server = $this->getMock('Wrench\Server', array(), array(), '', false);
|
||||
$server->registerApplication($path, $application);
|
||||
|
||||
$manager->expects($this->any())
|
||||
->method('getApplicationForPath')
|
||||
->with($path)
|
||||
->will($this->returnValue($application));
|
||||
|
||||
$manager->expects($this->exactly(isset($counts['removeConnection']) ? $counts['removeConnection'] : 0))
|
||||
->method('removeConnection');
|
||||
|
||||
$manager->expects($this->any())
|
||||
->method('getServer')
|
||||
->will($this->returnValue($server));
|
||||
|
||||
$connection = $this->getInstance($manager, $socket);
|
||||
|
||||
return $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ConnectionManager
|
||||
*/
|
||||
protected function getMockConnectionManager()
|
||||
{
|
||||
return $this->getMock('Wrench\ConnectionManager', array(), array(), '', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a mock socket
|
||||
*
|
||||
* @return Socket
|
||||
*/
|
||||
protected function getMockSocket()
|
||||
{
|
||||
return $this->getMock('Wrench\Socket\ServerClientSocket', array(), array(), '', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a mock application
|
||||
*
|
||||
* @return EchoApplication
|
||||
*/
|
||||
protected function getMockApplication()
|
||||
{
|
||||
return $this->getMock('Wrench\Application\EchoApplication');
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider
|
||||
*
|
||||
* @return array<array<int>>
|
||||
*/
|
||||
public function getValidCloseCodes()
|
||||
{
|
||||
$arguments = array();
|
||||
foreach (Protocol::$closeReasons as $code => $reason) {
|
||||
$arguments[] = array($code);
|
||||
}
|
||||
return $arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider
|
||||
*
|
||||
* @return array<array<mixed>>
|
||||
*/
|
||||
public function getValidConstructorArguments()
|
||||
{
|
||||
$socket = $this->getMockSocket();
|
||||
|
||||
$socket->expects($this->any())
|
||||
->method('getIp')
|
||||
->will($this->returnValue('127.0.0.1'));
|
||||
|
||||
$socket->expects($this->any())
|
||||
->method('getPort')
|
||||
->will($this->returnValue(mt_rand(1025, 50000)));
|
||||
|
||||
$manager = $this->getMockConnectionManager();
|
||||
|
||||
return array(
|
||||
array(
|
||||
$manager,
|
||||
$socket,
|
||||
array('logger' => function() {})
|
||||
),
|
||||
array(
|
||||
$manager,
|
||||
$socket,
|
||||
array('logger' => function () {},
|
||||
'connection_id_algo' => 'sha512')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider
|
||||
*
|
||||
* Uses this awkward valid request array so that splitting of payloads
|
||||
* across multiple calls to handle can be tested
|
||||
*
|
||||
* testHandle($path, $request_handshake, array $requests, array $counts)
|
||||
*/
|
||||
public function getValidHandleData()
|
||||
{
|
||||
$valid_requests = array(
|
||||
array(
|
||||
'data' => array(
|
||||
"\x81\xad\x2e\xab\x82\xac\x6f\xfe\xd6\xe4\x14\x8b\xf9\x8c\x0c"
|
||||
."\xde\xf1\xc9\x5c\xc5\xe3\xc1\x4b\x89\xb8\x8c\x0c\xcd\xed\xc3"
|
||||
."\x0c\x87\xa2\x8e\x5e\xca\xf1\xdf\x59\xc4\xf0\xc8\x0c\x91\xa2"
|
||||
."\x8e\x4c\xca\xf0\x8e\x53\x81\xad\xd4\xfd\x81\xfe\x95\xa8\xd5"
|
||||
."\xb6\xee\xdd\xfa\xde\xf6\x88\xf2\x9b\xa6\x93\xe0\x93\xb1\xdf"
|
||||
."\xbb\xde\xf6\x9b\xee\x91\xf6\xd1\xa1\xdc\xa4\x9c\xf2\x8d\xa3"
|
||||
."\x92\xf3\x9a\xf6\xc7\xa1\xdc\xb6\x9c\xf3\xdc\xa9\x81\x80\x8e"
|
||||
."\x12\xcd\x8e\x81\x8c\xf6\x8a\xf0\xee\x9a\xeb\x83\x9a\xd6\xe7"
|
||||
."\x95\x9d\x85\xeb\x97\x8b" // Four text frames
|
||||
),
|
||||
'counts' => array(
|
||||
'onData' => 4
|
||||
)
|
||||
),
|
||||
array(
|
||||
'data' => array(
|
||||
"\x88\x80\xdc\x8e\xa2\xc5" // Close frame
|
||||
),
|
||||
'counts' => array(
|
||||
'removeConnection' => 1
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
$data = array();
|
||||
|
||||
$handshakes = $this->getValidHandshakeData();
|
||||
|
||||
foreach ($handshakes as $handshake) {
|
||||
foreach ($valid_requests as $handle_args) {
|
||||
$arguments = $handshake;
|
||||
$arguments[] = $handle_args['data'];
|
||||
$arguments[] = $handle_args['counts'];
|
||||
|
||||
$data[] = $arguments;
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider
|
||||
*/
|
||||
public function getValidHandshakeData()
|
||||
{
|
||||
return array(
|
||||
array(
|
||||
'/chat',
|
||||
"GET /chat HTTP/1.1\r
|
||||
Host: server.example.com\r
|
||||
Upgrade: websocket\r
|
||||
Connection: Upgrade\r
|
||||
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r
|
||||
Origin: http://example.com\r
|
||||
Sec-WebSocket-Version: 13\r\n\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider
|
||||
*/
|
||||
public function getWrongPathHandshakeData()
|
||||
{
|
||||
return array(
|
||||
array(
|
||||
'/foobar',
|
||||
"GET /chat HTTP/1.1\r
|
||||
Host: server.example.com\r
|
||||
Upgrade: websocket\r
|
||||
Connection: Upgrade\r
|
||||
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r
|
||||
Origin: http://example.com\r
|
||||
Sec-WebSocket-Version: 13\r\n\r\n"
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
29
listeners/lib/Wrench/Tests/Frame/BaseSubclassFrameTest.php
Executable file
29
listeners/lib/Wrench/Tests/Frame/BaseSubclassFrameTest.php
Executable file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Tests\Frame;
|
||||
|
||||
use Wrench\Frame\HybiFrame;
|
||||
use Wrench\Tests\Test;
|
||||
|
||||
class BadSubclassFrame extends HybiFrame
|
||||
{
|
||||
protected $payload = 'asdmlasdkm';
|
||||
protected $buffer = false;
|
||||
}
|
||||
|
||||
class BadSubclassFrameTest extends Test
|
||||
{
|
||||
/**
|
||||
* @expectedException Wrench\Exception\FrameException
|
||||
*/
|
||||
public function testInvalidFrameBuffer()
|
||||
{
|
||||
$frame = new BadSubclassFrame();
|
||||
$frame->getFrameBuffer();
|
||||
}
|
||||
|
||||
protected function getClass()
|
||||
{
|
||||
return 'Wrench\Tests\Frame\BadSubclassFrame';
|
||||
}
|
||||
}
|
168
listeners/lib/Wrench/Tests/Frame/FrameTest.php
Executable file
168
listeners/lib/Wrench/Tests/Frame/FrameTest.php
Executable file
@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Tests\Frame;
|
||||
|
||||
use Wrench\Protocol\Protocol;
|
||||
use Wrench\Frame\Frame;
|
||||
use Wrench\Tests\Test;
|
||||
use \Exception;
|
||||
|
||||
/**
|
||||
* Frame test
|
||||
*/
|
||||
abstract class FrameTest extends Test
|
||||
{
|
||||
/**
|
||||
* A fresh instance of the class being tested
|
||||
*
|
||||
* @var Frame
|
||||
*/
|
||||
protected $frame;
|
||||
|
||||
/**
|
||||
* @see PHPUnit_Framework_TestCase::setUp()
|
||||
*/
|
||||
public function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
$this->frame = $this->getNewFrame();
|
||||
}
|
||||
|
||||
protected function getNewFrame()
|
||||
{
|
||||
$class = $this->getClass();
|
||||
return new $class();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see PHPUnit_Framework_TestCase::tearDown()
|
||||
*/
|
||||
protected function tearDown()
|
||||
{
|
||||
parent::tearDown();
|
||||
unset($this->frame);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $payload
|
||||
* @dataProvider getValidEncodePayloads
|
||||
*/
|
||||
public function testBijection($type, $payload, $masked)
|
||||
{
|
||||
// Encode the payload
|
||||
$this->frame->encode($payload, $type, $masked);
|
||||
|
||||
// Get the resulting buffer
|
||||
$buffer = $this->frame->getFrameBuffer();
|
||||
$this->assertTrue((boolean)$buffer, 'Got raw frame buffer');
|
||||
|
||||
// And feed it back into a new frame
|
||||
$frame = $this->getNewFrame();
|
||||
$frame->receiveData($buffer);
|
||||
|
||||
// Check the properties of the new frame against the old, all match
|
||||
$this->assertEquals(
|
||||
$this->frame->getType(),
|
||||
$frame->getType(),
|
||||
'Types match after encode -> receiveData'
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
$this->frame->getFramePayload(),
|
||||
$frame->getFramePayload(),
|
||||
'Payloads match after encode -> receiveData'
|
||||
);
|
||||
|
||||
// Masking key should not be different, because we read the buffer in directly
|
||||
$this->assertEquals(
|
||||
$this->frame->getFrameBuffer(),
|
||||
$frame->getFrameBuffer(),
|
||||
'Raw buffers match too'
|
||||
);
|
||||
|
||||
// This time, we create a new frame and read the data in with encode
|
||||
$frame = $this->getNewFrame();
|
||||
$frame->encode($this->frame->getFramePayload(), $type, $masked);
|
||||
|
||||
// These still match
|
||||
$this->assertEquals(
|
||||
$this->frame->getType(),
|
||||
$frame->getType(),
|
||||
'Types match after encode -> receiveData -> encode'
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
$this->frame->getFramePayload(),
|
||||
$frame->getFramePayload(),
|
||||
'Payloads match after encode -> receiveData -> encode'
|
||||
);
|
||||
|
||||
// But the masking key should be different, thus, so are the buffers
|
||||
if ($masked) {
|
||||
$this->assertNotEquals(
|
||||
$this->frame->getFrameBuffer(),
|
||||
$frame->getFrameBuffer(),
|
||||
'Raw buffers don\'t match because of masking'
|
||||
);
|
||||
} else {
|
||||
$this->assertEquals(
|
||||
$this->frame->getFramePayload(),
|
||||
$frame->getFramePayload(),
|
||||
'Payloads match after encode -> receiveData -> encode'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $payload
|
||||
* @dataProvider getValidEncodePayloads
|
||||
*/
|
||||
public function testEncodeTypeReflection($type, $payload, $masked)
|
||||
{
|
||||
$this->frame->encode($payload, $type);
|
||||
$this->assertEquals(Protocol::TYPE_TEXT, $this->frame->getType(), 'Encode retains type information');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $payload
|
||||
* @dataProvider getValidEncodePayloads
|
||||
*/
|
||||
public function testEncodeLengthReflection($type, $payload, $masked)
|
||||
{
|
||||
$this->frame->encode($payload, $type);
|
||||
$this->assertEquals(strlen($payload), $this->frame->getLength(), 'Encode does not alter payload length');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $payload
|
||||
* @dataProvider getValidEncodePayloads
|
||||
*/
|
||||
public function testEncodePayloadReflection($type, $payload, $masked)
|
||||
{
|
||||
$this->frame->encode($payload, $type, $masked);
|
||||
$this->assertEquals($payload, $this->frame->getFramePayload(), 'Encode retains payload information');
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getValidEncodePayloads()
|
||||
{
|
||||
return array(
|
||||
array(
|
||||
Protocol::TYPE_TEXT,
|
||||
"123456\x007890!@#$%^&*()qwe\trtyuiopQWERTYUIOPasdfghjklASFGH\n
|
||||
JKLzxcvbnmZXCVBNM,./<>?;[]{}-=_+\|'asdad0x11\aasdassasdasasdsd",
|
||||
true
|
||||
),
|
||||
array(
|
||||
Protocol::TYPE_TEXT,
|
||||
pack('CCCCCCC', 0x00, 0x01, 0x02, 0x03, 0x04, 0xff, 0xf0),
|
||||
true
|
||||
),
|
||||
array(Protocol::TYPE_TEXT, ' ', true)
|
||||
);
|
||||
}
|
||||
}
|
14
listeners/lib/Wrench/Tests/Frame/HybiFrameTest.php
Executable file
14
listeners/lib/Wrench/Tests/Frame/HybiFrameTest.php
Executable file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Tests\Frame;
|
||||
|
||||
use Wrench\Frame\HybiFrame;
|
||||
use Wrench\Tests\Frame\FrameTest;
|
||||
|
||||
class HybiFrameTest extends FrameTest
|
||||
{
|
||||
protected function getClass()
|
||||
{
|
||||
return 'Wrench\Frame\HybiFrame';
|
||||
}
|
||||
}
|
23
listeners/lib/Wrench/Tests/Listener/ListenerTest.php
Executable file
23
listeners/lib/Wrench/Tests/Listener/ListenerTest.php
Executable file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Tests\Listener;
|
||||
|
||||
use Wrench\Tests\Test;
|
||||
|
||||
/**
|
||||
* Payload test
|
||||
*/
|
||||
abstract class ListenerTest extends Test
|
||||
{
|
||||
/**
|
||||
* @depends testConstructor
|
||||
*/
|
||||
public function testListen($instance)
|
||||
{
|
||||
$server = $this->getMock('Wrench\Server', array(), array(), '', false);
|
||||
|
||||
$instance->listen($server);
|
||||
}
|
||||
|
||||
abstract public function testConstructor();
|
||||
}
|
110
listeners/lib/Wrench/Tests/Listener/OriginPolicyTest.php
Executable file
110
listeners/lib/Wrench/Tests/Listener/OriginPolicyTest.php
Executable file
@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Tests\Listener;
|
||||
|
||||
use Wrench\Listener\RateLimiter;
|
||||
use Wrench\Tests\Listener\ListenerTest;
|
||||
|
||||
class OriginPolicyTest extends ListenerTest
|
||||
{
|
||||
/**
|
||||
* @see Wrench\Tests.Test::getClass()
|
||||
*/
|
||||
public function getClass()
|
||||
{
|
||||
return 'Wrench\Listener\OriginPolicy';
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Wrench\Tests\Listener.ListenerTest::testConstructor()
|
||||
*/
|
||||
public function testConstructor()
|
||||
{
|
||||
$instance = $this->getInstance(array());
|
||||
$this->assertInstanceOfClass($instance, 'No constructor arguments');
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getValidArguments
|
||||
* @param array $allowed
|
||||
* @param string $domain
|
||||
*/
|
||||
public function testValidAllowed($allowed, $domain)
|
||||
{
|
||||
$instance = $this->getInstance($allowed);
|
||||
$this->assertTrue($instance->isAllowed($domain));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getValidArguments
|
||||
* @param array $allowed
|
||||
* @param string $domain
|
||||
*/
|
||||
public function testValidHandshake($allowed, $domain)
|
||||
{
|
||||
$instance = $this->getInstance($allowed);
|
||||
|
||||
$connection = $this->getMock('Wrench\Connection', array(), array(), '', false);
|
||||
|
||||
$connection
|
||||
->expects($this->never())
|
||||
->method('close');
|
||||
|
||||
$instance->onHandshakeRequest($connection, '/', $domain, 'abc', array());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getInvalidArguments
|
||||
* @param array $allowed
|
||||
* @param string $bad_domain
|
||||
*/
|
||||
public function testInvalidAllowed($allowed, $bad_domain)
|
||||
{
|
||||
$instance = $this->getInstance($allowed);
|
||||
$this->assertFalse($instance->isAllowed($bad_domain));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getInvalidArguments
|
||||
* @param array $allowed
|
||||
* @param string $domain
|
||||
*/
|
||||
public function testInvalidHandshake($allowed, $bad_domain)
|
||||
{
|
||||
$instance = $this->getInstance($allowed);
|
||||
|
||||
$connection = $this->getMock('Wrench\Connection', array(), array(), '', false);
|
||||
|
||||
$connection
|
||||
->expects($this->once())
|
||||
->method('close');
|
||||
|
||||
$instance->onHandshakeRequest($connection, '/', $bad_domain, 'abc', array());
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider
|
||||
*/
|
||||
public function getValidArguments()
|
||||
{
|
||||
return array(
|
||||
array(array('localhost'), 'http://localhost'),
|
||||
array(array('foobar.com'), 'https://foobar.com'),
|
||||
array(array('https://foobar.com'), 'https://foobar.com')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider
|
||||
*/
|
||||
public function getInvalidArguments()
|
||||
{
|
||||
return array(
|
||||
array(array('localhost'), 'localdomain'),
|
||||
array(array('foobar.com'), 'foobar.org'),
|
||||
array(array('https://foobar.com'), 'http://foobar.com'),
|
||||
array(array('http://foobar.com'), 'foobar.com')
|
||||
);
|
||||
}
|
||||
}
|
67
listeners/lib/Wrench/Tests/Listener/RateLimiterTest.php
Executable file
67
listeners/lib/Wrench/Tests/Listener/RateLimiterTest.php
Executable file
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Tests\Listener;
|
||||
|
||||
use Wrench\Listener\RateLimiter;
|
||||
use Wrench\Tests\Listener\ListenerTest;
|
||||
|
||||
class RateLimiterTest extends ListenerTest
|
||||
{
|
||||
/**
|
||||
* @see Wrench\Tests.Test::getClass()
|
||||
*/
|
||||
public function getClass()
|
||||
{
|
||||
return 'Wrench\Listener\RateLimiter';
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Wrench\Tests\Listener.ListenerTest::testConstructor()
|
||||
*/
|
||||
public function testConstructor()
|
||||
{
|
||||
$instance = $this->getInstance();
|
||||
$this->assertInstanceOfClass($instance, 'No constructor arguments');
|
||||
return $instance;
|
||||
}
|
||||
|
||||
public function testOnSocketConnect()
|
||||
{
|
||||
$this->getInstance()->onSocketConnect(null, $this->getConnection());
|
||||
}
|
||||
|
||||
public function testOnSocketDisconnect()
|
||||
{
|
||||
$this->getInstance()->onSocketDisconnect(null, $this->getConnection());
|
||||
}
|
||||
|
||||
public function testOnClientData()
|
||||
{
|
||||
$this->getInstance()->onClientData(null, $this->getConnection());
|
||||
}
|
||||
|
||||
protected function getConnection()
|
||||
{
|
||||
$connection = $this->getMock('Wrench\Connection', array(), array(), '', false);
|
||||
|
||||
$connection
|
||||
->expects($this->any())
|
||||
->method('getIp')
|
||||
->will($this->returnValue('127.0.0.1'));
|
||||
|
||||
$connection
|
||||
->expects($this->any())
|
||||
->method('getId')
|
||||
->will($this->returnValue('abcdef01234567890'));
|
||||
|
||||
$manager = $this->getMock('Wrench\ConnectionManager', array(), array(), '', false);
|
||||
$manager->expects($this->any())->method('count')->will($this->returnValue(5));
|
||||
|
||||
$connection
|
||||
->expects($this->any())
|
||||
->method('getConnectionManager')
|
||||
->will($this->returnValue($manager));
|
||||
|
||||
return $connection;
|
||||
}
|
||||
}
|
14
listeners/lib/Wrench/Tests/Payload/HybiPayloadTest.php
Executable file
14
listeners/lib/Wrench/Tests/Payload/HybiPayloadTest.php
Executable file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Tests\Protocol;
|
||||
|
||||
use Wrench\Payload\HybiPayload;
|
||||
use Wrench\Tests\Payload\PayloadTest;
|
||||
|
||||
class HybiPayloadTest extends PayloadTest
|
||||
{
|
||||
protected function getClass()
|
||||
{
|
||||
return 'Wrench\Payload\HybiPayload';
|
||||
}
|
||||
}
|
140
listeners/lib/Wrench/Tests/Payload/PayloadTest.php
Executable file
140
listeners/lib/Wrench/Tests/Payload/PayloadTest.php
Executable file
@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Tests\Payload;
|
||||
|
||||
use Wrench\Protocol\Protocol;
|
||||
use Wrench\Payload\Payload;
|
||||
use Wrench\Tests\Test;
|
||||
use \Exception;
|
||||
|
||||
/**
|
||||
* Payload test
|
||||
*/
|
||||
abstract class PayloadTest extends Test
|
||||
{
|
||||
/**
|
||||
* A fresh instance of the class being tested
|
||||
*
|
||||
* @var Payload
|
||||
*/
|
||||
protected $payload;
|
||||
|
||||
/**
|
||||
* @see PHPUnit_Payloadwork_TestCase::setUp()
|
||||
*/
|
||||
public function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->payload = $this->getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the constructor
|
||||
*/
|
||||
public function testConstructor()
|
||||
{
|
||||
$this->assertInstanceOfClass($this->getInstance());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $payload
|
||||
* @dataProvider getValidEncodePayloads
|
||||
*/
|
||||
public function testBijection($type, $payload)
|
||||
{
|
||||
// Encode the payload
|
||||
$this->payload->encode($payload, $type);
|
||||
|
||||
// Create a new payload and read the data in with encode
|
||||
$payload = $this->getInstance();
|
||||
$payload->encode($this->payload->getPayload(), $type);
|
||||
|
||||
// These still match
|
||||
$this->assertEquals(
|
||||
$this->payload->getType(),
|
||||
$payload->getType(),
|
||||
'Types match after encode -> receiveData'
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
$this->payload->getPayload(),
|
||||
$payload->getPayload(),
|
||||
'Payloads match after encode -> receiveData'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $payload
|
||||
* @dataProvider getValidEncodePayloads
|
||||
*/
|
||||
public function testEncodeTypeReflection($type, $payload)
|
||||
{
|
||||
$this->payload->encode($payload, Protocol::TYPE_TEXT);
|
||||
$this->assertEquals(Protocol::TYPE_TEXT, $this->payload->getType(), 'Encode retains type information');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $payload
|
||||
* @dataProvider getValidEncodePayloads
|
||||
*/
|
||||
public function testEncodePayloadReflection($type, $payload)
|
||||
{
|
||||
$this->payload->encode($payload, Protocol::TYPE_TEXT);
|
||||
$this->assertEquals($payload, $this->payload->getPayload(), 'Encode retains payload information');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests sending to a socket
|
||||
* @dataProvider getValidEncodePayloads
|
||||
*/
|
||||
public function testSendToSocket($type, $payload)
|
||||
{
|
||||
$successfulSocket = $this->getMock('Wrench\Socket\ClientSocket', array(), array('wss://localhost:8000'));
|
||||
$failedSocket = clone $successfulSocket;
|
||||
|
||||
$successfulSocket->expects($this->any())
|
||||
->method('send')
|
||||
->will($this->returnValue(true));
|
||||
|
||||
$failedSocket->expects($this->any())
|
||||
->method('send')
|
||||
->will($this->returnValue(false));
|
||||
|
||||
$this->payload->encode($payload, $type);
|
||||
|
||||
$this->assertTrue($this->payload->sendToSocket($successfulSocket));
|
||||
$this->assertFalse($this->payload->sendToSocket($failedSocket));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests receiving data
|
||||
* @dataProvider getValidEncodePayloads
|
||||
*/
|
||||
public function testReceieveData($type, $payload)
|
||||
{
|
||||
$payload = $this->getInstance();
|
||||
$payload->receiveData($payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getValidEncodePayloads()
|
||||
{
|
||||
return array(
|
||||
array(
|
||||
Protocol::TYPE_TEXT,
|
||||
"123456\x007890!@#$%^&*()qwe\trtyuiopQWERTYUIOPasdfghjklASFGH\n
|
||||
JKLzxcvbnmZXCVBNM,./<>?;[]{}-=_+\|'asdad0x11\aasdassasdasasdsd"
|
||||
),
|
||||
array(
|
||||
Protocol::TYPE_TEXT,
|
||||
pack('CCCCCCC', 0x00, 0x01, 0x02, 0x03, 0x04, 0xff, 0xf0)
|
||||
),
|
||||
array(Protocol::TYPE_TEXT, ' ')
|
||||
);
|
||||
}
|
||||
}
|
180
listeners/lib/Wrench/Tests/Protocol/ProtocolTest.php
Executable file
180
listeners/lib/Wrench/Tests/Protocol/ProtocolTest.php
Executable file
@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Tests\Protocol;
|
||||
|
||||
use Wrench\Tests\Test;
|
||||
use \Exception;
|
||||
|
||||
abstract class ProtocolTest extends Test
|
||||
{
|
||||
/**
|
||||
* @see PHPUnit_Framework_TestCase::setUp()
|
||||
*/
|
||||
public function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getValidHandshakeRequests
|
||||
*/
|
||||
public function testValidatHandshakeRequestValid($request)
|
||||
{
|
||||
try {
|
||||
list($path, $origin, $key, $extensions, $protocol) = $this->getInstance()->validateRequestHandshake($request);
|
||||
|
||||
$this->assertEquals('/chat', $path);
|
||||
$this->assertEquals('http://example.com', $origin);
|
||||
$this->assertEquals('dGhlIHNhbXBsZSBub25jZQ==', $key);
|
||||
$this->assertTrue(is_array($extensions), 'Extensions returned as array');
|
||||
$this->assertEquals(array('x-test', 'x-test2'), $extensions, 'Extensions match');
|
||||
$this->assertEquals('chat, superchat', $protocol);
|
||||
} catch (Exception $e) {
|
||||
$this->fail($e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getValidHandshakeResponses
|
||||
*/
|
||||
public function testValidateHandshakeResponseValid($response, $key)
|
||||
{
|
||||
try {
|
||||
$valid = $this->getInstance()->validateResponseHandshake($response, $key);
|
||||
$this->assertTrue(is_bool($valid), 'Validation return value is boolean');
|
||||
$this->assertTrue($valid, 'Handshake response validates');
|
||||
} catch (Exception $e) {
|
||||
$this->fail('Validated valid response handshake as invalid');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getValidHandshakeResponses
|
||||
*/
|
||||
public function testGetResponseHandsake($unused, $key)
|
||||
{
|
||||
try {
|
||||
$response = $this->getInstance()->getResponseHandshake($key);
|
||||
$this->assertHttpResponse($response);
|
||||
} catch (Exception $e) {
|
||||
$this->fail('Unable to get handshake response: ' . $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts the string response is an HTTP response
|
||||
*
|
||||
* @param string $response
|
||||
*/
|
||||
protected function assertHttpResponse($response, $message = '')
|
||||
{
|
||||
$this->assertStringStartsWith('HTTP', $response, $message . ' - response starts well');
|
||||
$this->assertStringEndsWith("\r\n", $response, $message . ' - response ends well');
|
||||
}
|
||||
|
||||
public function testGetVersion()
|
||||
{
|
||||
$version = $this->getInstance()->getVersion();
|
||||
$this->assertTrue(is_int($version));
|
||||
}
|
||||
|
||||
public function testGetResponseError()
|
||||
{
|
||||
$response = $this->getInstance()->getResponseError(400);
|
||||
$this->assertHttpResponse($response, 'Code as int');
|
||||
|
||||
$response = $this->getInstance()->getResponseError(new Exception('Some message', 500));
|
||||
$this->assertHttpResponse($response, 'Code in Exception');
|
||||
|
||||
$response = $this->getInstance()->getResponseError(888);
|
||||
$this->assertHttpResponse($response, 'Invalid code produces unimplemented response');
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getValidOriginUris
|
||||
*/
|
||||
public function testValidateOriginUriValid($uri)
|
||||
{
|
||||
try {
|
||||
$this->getInstance()->validateOriginUri($uri);
|
||||
} catch (\Exception $e) {
|
||||
$this->fail('Valid URI validated as invalid: ' . $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getInvalidOriginUris
|
||||
* @expectedException InvalidArgumentException
|
||||
*/
|
||||
public function testValidateOriginUriInvalid($uri)
|
||||
{
|
||||
$this->getInstance()->validateOriginUri($uri);
|
||||
}
|
||||
|
||||
public function getValidOriginUris()
|
||||
{
|
||||
return array(
|
||||
array('http://www.example.org'),
|
||||
array('http://www.example.com/some/page'),
|
||||
array('https://localhost/')
|
||||
);
|
||||
}
|
||||
|
||||
public function getInvalidOriginUris()
|
||||
{
|
||||
return array(
|
||||
array(false),
|
||||
array(true),
|
||||
array(''),
|
||||
array('blah')
|
||||
);
|
||||
}
|
||||
|
||||
public function getValidHandshakeRequests()
|
||||
{
|
||||
$cases = array();
|
||||
|
||||
|
||||
$cases[] = array("GET /chat HTTP/1.1\r
|
||||
Host: server.example.com\r
|
||||
Upgrade: websocket\r
|
||||
Connection: Upgrade\r
|
||||
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r
|
||||
Origin: http://example.com\r
|
||||
Sec-WebSocket-Extensions: x-test\r
|
||||
Sec-WebSocket-Extensions: x-test2\r
|
||||
Sec-WebSocket-Protocol: chat, superchat\r
|
||||
Sec-WebSocket-Version: 13\r
|
||||
\r\n");
|
||||
|
||||
$cases[] = array("GET /chat HTTP/1.1\r
|
||||
Host: server.example.com\r
|
||||
Upgrade: Websocket\r
|
||||
Connection: Upgrade\r
|
||||
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r
|
||||
Origin: http://example.com\r
|
||||
Sec-WebSocket-Extensions: x-test\r
|
||||
Sec-WebSocket-Extensions: x-test2\r
|
||||
Sec-WebSocket-Protocol: chat, superchat\r
|
||||
Sec-WebSocket-Version: 13\r
|
||||
\r\n");
|
||||
|
||||
return $cases;
|
||||
}
|
||||
|
||||
public function getValidHandshakeResponses()
|
||||
{
|
||||
$cases = array();
|
||||
|
||||
for ($i = 10; $i > 0; $i--) {
|
||||
$key = sha1(time() . uniqid('', true));
|
||||
$response = "Sec-WebSocket-Accept: "
|
||||
. base64_encode(sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true))
|
||||
. "\r\n\r\n";
|
||||
|
||||
$cases[] = array($response, $key);
|
||||
}
|
||||
|
||||
return $cases;
|
||||
}
|
||||
}
|
14
listeners/lib/Wrench/Tests/Protocol/Rfc6455ProtocolTest.php
Executable file
14
listeners/lib/Wrench/Tests/Protocol/Rfc6455ProtocolTest.php
Executable file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Tests\Protocol;
|
||||
|
||||
use Wrench\Protocol\Rfc6455Protocol;
|
||||
use Wrench\Tests\Protocol\ProtocolTest;
|
||||
|
||||
class Rfc6455ProtocolTest extends ProtocolTest
|
||||
{
|
||||
protected function getClass()
|
||||
{
|
||||
return 'Wrench\Protocol\Rfc6455Protocol';
|
||||
}
|
||||
}
|
78
listeners/lib/Wrench/Tests/ServerTest.php
Executable file
78
listeners/lib/Wrench/Tests/ServerTest.php
Executable file
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Tests;
|
||||
|
||||
use Wrench\Server;
|
||||
use Wrench\Tests\Test;
|
||||
use Wrench\Socket;
|
||||
|
||||
use \InvalidArgumentException;
|
||||
use \PHPUnit_Framework_Error;
|
||||
|
||||
/**
|
||||
* Tests the Server class
|
||||
*/
|
||||
class ServerTest extends Test
|
||||
{
|
||||
/**
|
||||
* @see Wrench\Tests.Test::getClass()
|
||||
*/
|
||||
protected function getClass()
|
||||
{
|
||||
return 'Wrench\Server';
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the constructor
|
||||
*
|
||||
* @param string $url
|
||||
* @param array $options
|
||||
* @dataProvider getValidConstructorArguments
|
||||
*/
|
||||
public function testConstructor($url, array $options = array())
|
||||
{
|
||||
$this->assertInstanceOfClass(
|
||||
$this->getInstance($url, $options),
|
||||
'Valid constructor arguments'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests logging
|
||||
*/
|
||||
public function testLogging()
|
||||
{
|
||||
$test = $this;
|
||||
$logged = false;
|
||||
|
||||
$server = $this->getInstance('ws://localhost:8000', array(
|
||||
'logger' => function ($message, $priority) use ($test, &$logged) {
|
||||
$test->assertTrue(is_string($message), 'Log had a string message');
|
||||
$test->assertTrue(is_string($priority), 'Log had a string priority');
|
||||
$logged = true;
|
||||
}
|
||||
));
|
||||
|
||||
$this->assertTrue($logged, 'The log callback was hit');
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider
|
||||
*
|
||||
* @return array<array<mixed>>
|
||||
*/
|
||||
public function getValidConstructorArguments()
|
||||
{
|
||||
return array(
|
||||
array(
|
||||
'ws://localhost:8000',
|
||||
array('logger' => array($this, 'log'))
|
||||
),
|
||||
array(
|
||||
'ws://localhost',
|
||||
array('logger' => array($this, 'log'))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
144
listeners/lib/Wrench/Tests/ServerTestHelper.php
Executable file
144
listeners/lib/Wrench/Tests/ServerTestHelper.php
Executable file
@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Tests;
|
||||
|
||||
/**
|
||||
* In conjunction with server.php, provides a listening server
|
||||
* against which tests can be run.
|
||||
*/
|
||||
class ServerTestHelper
|
||||
{
|
||||
const TEST_SERVER_PORT_MIN = 16666;
|
||||
const TEST_SERVER_PORT_MAX = 52222;
|
||||
|
||||
public static $nextPort = null;
|
||||
|
||||
protected $port = null;
|
||||
protected $process = null;
|
||||
protected $pipes = array();
|
||||
|
||||
/**
|
||||
* Gets the next available port number to start a server on
|
||||
*/
|
||||
public static function getNextPort()
|
||||
{
|
||||
if (self::$nextPort === null) {
|
||||
self::$nextPort = mt_rand(self::TEST_SERVER_PORT_MIN, self::TEST_SERVER_PORT_MAX);
|
||||
}
|
||||
return self::$nextPort++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destructor
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
$this->tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getConnectionString()
|
||||
{
|
||||
return 'ws://localhost:' . $this->port;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getEchoConnectionString()
|
||||
{
|
||||
return $this->getConnectionString() . '/echo';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the server process and sleeps for a few seconds while
|
||||
* it wakes up
|
||||
*/
|
||||
public function setUp()
|
||||
{
|
||||
$this->port = self::getNextPort();
|
||||
|
||||
$this->process = proc_open(
|
||||
$this->getCommand(),
|
||||
array(
|
||||
0 => array('file', '/dev/null', 'r'),
|
||||
1 => array('file', __DIR__ . '/../../../build/server.log', 'a+'),
|
||||
2 => array('file', __DIR__ . '/../../../build/server.err.log', 'a+')
|
||||
),
|
||||
$this->pipes,
|
||||
__DIR__ . '../'
|
||||
);
|
||||
|
||||
sleep(3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tears down the server process
|
||||
*
|
||||
* This method *must* be called
|
||||
*/
|
||||
public function tearDown()
|
||||
{
|
||||
if ($this->process) {
|
||||
foreach ($this->pipes as &$pipe) {
|
||||
fclose($pipe);
|
||||
}
|
||||
$this->pipes = null;
|
||||
|
||||
// Sigh
|
||||
$status = proc_get_status($this->process);
|
||||
|
||||
if ($status && isset($status['pid']) && $status['pid']) {
|
||||
// More sigh, this is the pid of the parent sh process, we want
|
||||
// to terminate the server directly
|
||||
$this->log('Command: /bin/ps -ao pid,ppid | /usr/bin/col | /usr/bin/tail -n +2 | /bin/grep \' ' . $status['pid'] . "'", 'info');
|
||||
exec('/bin/ps -ao pid,ppid | /usr/bin/col | /usr/bin/tail -n +2 | /bin/grep \' ' . $status['pid'] . "'", $processes, $return);
|
||||
|
||||
if ($return === 0) {
|
||||
foreach ($processes as $process) {
|
||||
list($pid, $ppid) = explode(' ', str_replace(' ', ' ', $process));
|
||||
if ($pid) {
|
||||
$this->log('Killing ' . $pid, 'info');
|
||||
exec('/bin/kill ' . $pid . ' > /dev/null 2>&1');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->log('Unable to find child processes', 'warning');
|
||||
}
|
||||
|
||||
sleep(1);
|
||||
|
||||
$this->log('Killing ' . $status['pid'], 'info');
|
||||
exec('/bin/kill ' . $status['pid'] . ' > /dev/null 2>&1');
|
||||
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
proc_close($this->process);
|
||||
$this->process = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the server command
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getCommand()
|
||||
{
|
||||
return sprintf('/usr/bin/env php %s/server.php %d', __DIR__, $this->port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message
|
||||
*
|
||||
* @param string $message
|
||||
* @param string $priority
|
||||
*/
|
||||
public function log($message, $priority = 'info')
|
||||
{
|
||||
//echo $message . "\n";
|
||||
}
|
||||
}
|
167
listeners/lib/Wrench/Tests/Socket/ClientSocketTest.php
Executable file
167
listeners/lib/Wrench/Tests/Socket/ClientSocketTest.php
Executable file
@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Tests\Socket;
|
||||
|
||||
use Wrench\Protocol\Rfc6455Protocol;
|
||||
use Wrench\Socket\ClientSocket;
|
||||
use Wrench\Tests\ServerTestHelper;
|
||||
use \Exception;
|
||||
use \stdClass;
|
||||
|
||||
class ClientSocketTest extends UriSocketTest
|
||||
{
|
||||
/**
|
||||
* @see Wrench\Tests.Test::getClass()
|
||||
*/
|
||||
public function getClass()
|
||||
{
|
||||
return 'Wrench\Socket\ClientSocket';
|
||||
}
|
||||
|
||||
/**
|
||||
* Overriden to use with the depends annotation
|
||||
*
|
||||
* @see Wrench\Tests\Socket.UriSocketTest::testConstructor()
|
||||
*/
|
||||
public function testConstructor()
|
||||
{
|
||||
$instance = parent::testConstructor();
|
||||
|
||||
$socket = null;
|
||||
|
||||
$this->assertInstanceOfClass(
|
||||
new ClientSocket('ws://localhost/'),
|
||||
'ws:// scheme, default port'
|
||||
);
|
||||
|
||||
$this->assertInstanceOfClass(
|
||||
new ClientSocket('ws://localhost/some-arbitrary-path'),
|
||||
'with path'
|
||||
);
|
||||
|
||||
$this->assertInstanceOfClass(
|
||||
new ClientSocket('wss://localhost/test', array()),
|
||||
'empty options'
|
||||
);
|
||||
|
||||
$this->assertInstanceOfClass(
|
||||
new ClientSocket('ws://localhost:8000/foo'),
|
||||
'specified port'
|
||||
);
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
public function testOptions()
|
||||
{
|
||||
$socket = null;
|
||||
|
||||
$this->assertInstanceOfClass(
|
||||
$socket = new ClientSocket(
|
||||
'ws://localhost:8000/foo', array(
|
||||
'timeout_connect' => 10
|
||||
)
|
||||
),
|
||||
'connect timeout'
|
||||
);
|
||||
|
||||
$this->assertInstanceOfClass(
|
||||
$socket = new ClientSocket(
|
||||
'ws://localhost:8000/foo', array(
|
||||
'timeout_socket' => 10
|
||||
)
|
||||
),
|
||||
'socket timeout'
|
||||
);
|
||||
|
||||
$this->assertInstanceOfClass(
|
||||
$socket = new ClientSocket(
|
||||
'ws://localhost:8000/foo', array(
|
||||
'protocol' => new Rfc6455Protocol()
|
||||
)
|
||||
),
|
||||
'protocol'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException InvalidArgumentException
|
||||
*/
|
||||
public function testProtocolTypeError()
|
||||
{
|
||||
$socket = new ClientSocket(
|
||||
'ws://localhost:8000/foo', array(
|
||||
'protocol' => new stdClass()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException PHPUnit_Framework_Error
|
||||
*/
|
||||
public function testConstructorUriUnspecified()
|
||||
{
|
||||
$w = new ClientSocket();
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException InvalidArgumentException
|
||||
*/
|
||||
public function testConstructorUriEmpty()
|
||||
{
|
||||
$w = new ClientSocket(null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @expectedException InvalidArgumentException
|
||||
*/
|
||||
public function testConstructorUriInvalid()
|
||||
{
|
||||
$w = new ClientSocket('Bad argument');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @depends testConstructor
|
||||
* @expectedException Wrench\Exception\SocketException
|
||||
*/
|
||||
public function testSendTooEarly($instance)
|
||||
{
|
||||
$instance->send('foo');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the connect, send, receive method
|
||||
*/
|
||||
public function testConnect()
|
||||
{
|
||||
try {
|
||||
$helper = new ServerTestHelper();
|
||||
$helper->setUp();
|
||||
|
||||
$instance = $this->getInstance($helper->getConnectionString());
|
||||
$success = $instance->connect();
|
||||
|
||||
$this->assertTrue($success, 'Client socket can connect to test server');
|
||||
|
||||
$sent = $instance->send("GET /echo HTTP/1.1\r
|
||||
Host: localhost\r
|
||||
Upgrade: websocket\r
|
||||
Connection: Upgrade\r
|
||||
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r
|
||||
Origin: http://localhost\r
|
||||
Sec-WebSocket-Version: 13\r\n\r\n");
|
||||
$this->assertNotEquals(false, $sent, 'Client socket can send to test server');
|
||||
|
||||
$response = $instance->receive();
|
||||
$this->assertStringStartsWith('HTTP', $response, 'Response looks like HTTP handshake response');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$helper->tearDown();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$helper->tearDown();
|
||||
}
|
||||
}
|
42
listeners/lib/Wrench/Tests/Socket/ServerClientSocketTest.php
Executable file
42
listeners/lib/Wrench/Tests/Socket/ServerClientSocketTest.php
Executable file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Tests\Socket;
|
||||
|
||||
use \Exception;
|
||||
|
||||
class ServerClientSocketTest extends SocketTest
|
||||
{
|
||||
public function getClass()
|
||||
{
|
||||
return 'Wrench\Socket\ServerClientSocket';
|
||||
}
|
||||
|
||||
/**
|
||||
* By default, the socket has not required arguments
|
||||
*/
|
||||
public function testConstructor()
|
||||
{
|
||||
$resource = null;
|
||||
$instance = $this->getInstance($resource);
|
||||
$this->assertInstanceOfClass($instance);
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException Wrench\Exception\SocketException
|
||||
* @depends testConstructor
|
||||
*/
|
||||
public function testGetIpTooSoon($instance)
|
||||
{
|
||||
$instance->getIp();
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException Wrench\Exception\SocketException
|
||||
* @depends testConstructor
|
||||
*/
|
||||
public function testGetPortTooSoon($instance)
|
||||
{
|
||||
$instance->getPort();
|
||||
}
|
||||
}
|
13
listeners/lib/Wrench/Tests/Socket/ServerSocketTest.php
Executable file
13
listeners/lib/Wrench/Tests/Socket/ServerSocketTest.php
Executable file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Tests\Socket;
|
||||
|
||||
use \Exception;
|
||||
|
||||
class ServerSocketTest extends UriSocketTest
|
||||
{
|
||||
public function getClass()
|
||||
{
|
||||
return 'Wrench\Socket\ServerSocket';
|
||||
}
|
||||
}
|
47
listeners/lib/Wrench/Tests/Socket/SocketTest.php
Executable file
47
listeners/lib/Wrench/Tests/Socket/SocketTest.php
Executable file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Tests\Socket;
|
||||
|
||||
use Wrench\Tests\Test;
|
||||
use \Exception;
|
||||
use Wrench\Socket\Socket;
|
||||
|
||||
abstract class SocketTest extends Test
|
||||
{
|
||||
/**
|
||||
* Require constructor testing
|
||||
*/
|
||||
abstract public function testConstructor();
|
||||
|
||||
/**
|
||||
* @depends testConstructor
|
||||
*/
|
||||
public function testIsConnected($instance)
|
||||
{
|
||||
$connected = $instance->isConnected();
|
||||
$this->assertTrue(is_bool($connected), 'isConnected returns boolean');
|
||||
$this->assertFalse($connected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getValidNames
|
||||
* @param string $name
|
||||
*/
|
||||
public function testGetNamePart($name, $ip, $port)
|
||||
{
|
||||
$this->assertEquals($ip, Socket::getNamePart($name, Socket::NAME_PART_IP), 'splits ip correctly');
|
||||
$this->assertEquals($port, Socket::getNamePart($name, Socket::NAME_PART_PORT), 'splits port correctly');
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider
|
||||
*/
|
||||
public function getValidNames()
|
||||
{
|
||||
return array(
|
||||
array('127.0.0.1:52339', '127.0.0.1', '52339'),
|
||||
array('255.255.255.255:1025', '255.255.255.255', '1025'),
|
||||
array('::1:56670', '::1', '56670')
|
||||
);
|
||||
}
|
||||
}
|
56
listeners/lib/Wrench/Tests/Socket/UriSocketTest.php
Executable file
56
listeners/lib/Wrench/Tests/Socket/UriSocketTest.php
Executable file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Wrench\Tests\Socket;
|
||||
|
||||
use \Exception;
|
||||
|
||||
abstract class UriSocketTest extends SocketTest
|
||||
{
|
||||
/**
|
||||
* By default, the socket has not required arguments
|
||||
*/
|
||||
public function testConstructor()
|
||||
{
|
||||
$instance = $this->getInstance('ws://localhost:8000');
|
||||
$this->assertInstanceOfClass($instance);
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getInvalidConstructorArguments
|
||||
* @expectedException InvalidArgumentException
|
||||
*/
|
||||
public function testInvalidConstructor($uri)
|
||||
{
|
||||
$this->getInstance($uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testConstructor
|
||||
*/
|
||||
public function testGetIp($instance)
|
||||
{
|
||||
$this->assertStringStartsWith('localhost', $instance->getIp(), 'Correct host');
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testConstructor
|
||||
*/
|
||||
public function testGetPort($instance)
|
||||
{
|
||||
$this->assertEquals(8000, $instance->getPort(), 'Correct port');
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider
|
||||
*/
|
||||
public function getInvalidConstructorArguments()
|
||||
{
|
||||
return array(
|
||||
array(false),
|
||||
array('http://www.google.com/'),
|
||||
array('ws:///'),
|
||||
array(':::::'),
|
||||
);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user