diff --git a/.gitignore b/.gitignore index e7c7906..2e1ff73 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ # Ignore generated log files. /bin/*.log -/bin/token.test \ No newline at end of file +/bin/token.test +_config.php \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e69de29 diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..443d190 --- /dev/null +++ b/.htaccess @@ -0,0 +1,22 @@ + + + +RewriteEngine On +RewriteBase / + +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule ^(transmogrify/(?:info|system|tests))/(.*)\.(.*)$ /lib/delegator-index.php?r=$2&t=$3&c=$1 [L] +RewriteRule ^(transmogrify/(?:info|system|tests))/$ /lib/delegator-index.php?r=$1&t=LIST [L] +RewriteRule ^(transmogrify/.*)\.(.*)$ /lib/delegator-index.php?r=$1&t=$2 [L] +RewriteRule ^(transmogrify/.*)$ /lib/delegator-index.php?r=$1&t=LIST [L] + +RewriteCond %{REQUEST_FILENAME} !-f +RewriteRule ^(.*)$ /docs/index.php?presto&page=$1 [NC,QSA,L] + + + +ErrorDocument 404 /docs/index.php?presto&error=404 +ErrorDocument 500 /docs/index.php?presto&error=500 +ErrorDocument 401 /docs/index.php?presto&error=401 +ErrorDocument 403 /docs/index.php?presto&error=403 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..863a500 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,122 @@ +# Changelog + +# 1.1 - *Sim Sala Bim* + +**Presto 1.1 breaks compatibility with 1.0 in a few important areas:** + +* Adds subfolder delegation +* Removes add_delegate*, URL, and other unused code (subfolder delegation is much simpler) +* Improves debugging (PHP errors to error log, added trace mode) +* Added unit tests +* Updated setup tests (to use internal API) +* `API` base startup has been simplified, removing the class pre-scanning mechanism. Presto will throw an exception if 1.0 usage is detected. +* API parameters are flattened to their obvious parts (get parameters, options, put/post body, resource type). This deprecates the `$ctx` object, which is still available via `self::$call`. +* `add_delegate` has been removed, as it has been replaced by folder delegation +* `add_filter` has been removed (a candidate 1.1 feature), as the other features provided enough utility on their own +* `Request::URI` has been removed in favour of a simpler builder function. URI parsing is now spread over `htaccess` for more obvious routing rules. + +## Alpha 1 features + +This patch adds a number of features planned for 1.1, including: + +* Dispatching to folders of APIs +* Preflighting dispatch calls for parameter and model processing +* Adds internal testing and debugging APIs (only available when enabled) +* Adds command line unit testing tools +* Adds a trace mode, and better exception logging +* Adds routing information to exceptions (if trace is enabled) + +## Updated parameter passing + +The calling convention for delegate API functions has changed from: + + public function get($ctx) { } + public function get_article($ctx) { } + public function put_article($ctx) { } + +To the more verbose form: + + public function get($params, $options, $body, $type) { } + public function get_article($params, $options, $body, $type) { } + public function put_article($params, $options, $body, $type) { } + +This makes the call parameters clearer than they were in the past. + +## Example folder delegation + +Folder delegation uses Apache rewriting to map folders of APIs to specific URLs. For example, Presto uses it for internal delegation to its `transmogrify` APIs: + + RewriteRule ^(transmogrify/(?:info|system|tests))/(.*)\.(.*)$ /lib/delegator-index.php?r=$2&t=$3&c=$1 [L] + +This routes all requests to 3 specific folders within `transmogrify` to files within the folders `info/`, `system/`, and `tests/` in `transmogrify/`. + +Requests like: + + get "transmogrify/system/httpd/headers/content-type.json" + +Are mapped to: + + transmogrify/system/httpd.php::httpd->get_headers(); + +## Example preflight delegation + +API calls can be split into two parts to simplify handling complex model creation and parameter processing. The split adds a preflight call to a *model* function of the same root name as the API itself. + +Consider a simple GET: + + get "transmogrify/system/httpd/headers/content-type.json" + +This call can optionally be split into the model creation and the API call itself. + +The model creation preflight call would be: + + http.php::http->get_headers_model($p = array('content-type')) { + return new Features($p); + } + +The call receives the standard API request parameters, returning a newly created model object of some sort. This call can be used to check parameters, check preconditions, and massage parameters into useful/predictable values for the API call itself: + + http.php::http->get_headers($featureModel) { + } + + +## Example routing trace + +If `PRESTO_TRACE` is enabled, exceptions will include: + + + [OPTIONS] transmogrify/info/delegation.json #0 [OK] + { + "_presto_trace": { + "request": "/transmogrify/info/delegation.json", + "routing_scheme": { + "action": "options", + "class": "delegation", + "container": "transmogrify/info", + "file": "transmogrify/info/delegation.php", + "method": "options", + "options": [], + "params": [], + "resource": "", + "type": "json" + }, + "version": "presto-v1.10" + }, + "code": 404, + "message": "Can't find delegation->options" + } + + +The `_presto_trace` element is added to make debugging easier. This trace is also available in the error log (in trace mode only): + + [Fri Feb 22 16:38:58 2013] [error] [client 127.0.0.1] PRESTO: _presto_trace, {"routing_scheme":{"container":"transmogrify\\/info","class":"delegation","file":"transmogrify\\/info\\/delegation.php","resource":"","type":"json","action":"get","method":"get","params":[],"options":[]},"request":"\\/transmogrify\\/info\\/delegation.json","version":"presto-v1.10"} + + +# 1.0 - A la peanut butter sandwiches + +* First public release +* Adds sub delegates and param filtering + +# 0.8 + +* Adds basic URI -> class/member delegation and auto data return by type diff --git a/INSTALL.md b/INSTALL.md index f1ce206..a9a0317 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -25,7 +25,7 @@ This file sets up Presto's delegator features, so that your classes are called i 4. Copy the example API file and test: -
$ cp lib/presto/examples/info.php .
+
$ cp lib/presto/setup-tests/info.php .
 $ curl [YOUR WEBROOT]/info.json	
 {"example":"This is some example information"}
@@ -41,4 +41,4 @@ The example API file writes a simple DOM (a `PHP` keyed array) as `JSON` back to You need to enable the JSON extension using a custom RC (not a custom .ini). Find the instructions here: -* http://wiki.dreamhost.com/PHP.ini#Loading_PHP_5.3_extensions_on_all_domains_.28on_VPS_or_dedicated.29 \ No newline at end of file +* http://wiki.dreamhost.com/PHP.ini#Loading_PHP_5.3_extensions_on_all_domains_.28on_VPS_or_dedicated.29 diff --git a/LICENSE b/LICENSE deleted file mode 100644 index a5c3517..0000000 --- a/LICENSE +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (C) 2010-2011 by Bruce Alderson - -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. \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..fa17ef1 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,12 @@ +# Free, free, share-alike + +Created by [Bruce Alderson](http://warpedvisions.org) (2011). + +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. + +**PS: use, extend, share, enjoy. Thanks!** \ No newline at end of file diff --git a/README.md b/README.md index 1e87650..0edfd38 100644 --- a/README.md +++ b/README.md @@ -1,86 +1,81 @@ -Presto - PHP REST toolkit -========================= +# Presto 1.1 -Presto is a small library for building simple, RESTful APIs using PHP. It's lightweight, decoupled, and focused on making web apps the right way; using APIs with clean URLs, which produce simple, clean output in standard formats like JSON, HTML, and XML. +*Frugal PHP + REST toolkit* -How is it different? --------------------- +Presto is a library for building RESTful APIs in PHP `5.3+`. It's lightweight, decoupled, and focused on making web apps the right way; using APIs and clean URLs to produce output in standard formats like JSON, HTML, and XML. It encourages separating views completely from your model and controller code, and to animate your user interfaces in HTML, CSS, and JavaScript. It also uses existing tools and libraries for what they're good at, like letting Apache deal with routing, and letting PHP load and execute a minimal amount of dynamic code. -Presto has no views, no models, and no controllers[^1]. Instead it focuses on APIs built from simple classes. It encourages applications to be based on APIs, which feed web applications animated with HTML/CSS/JavaScript. +## How is Presto different? -A Presto API is simply a PHP class that maps to a resource or tree of resources. It has: +Presto flattens standard MVC to fit REST requests better. It focuses on APIs built from simple classes, relying on the web server for routing and on PHP autoloading for delegating requests to class member calls. User interfaces are left to other toolkits, though Presto can serve up fragments of HTML with ease. Presto focuses on APIs. -* Public members that map to requests (`GET thing/details.json` would map to `$thing->get_details()`) -* Request parameters and input payloads that are packaged up and sanitized -* Each route returns data as DOMs, which are automatically adapted to the requested `ContentType` -* Errors are automatically converted into returned HTTP statuses +### A quick example -The resulting code is focused on resources and the rules for those resources, and not boilerplate, excessive error checking, routing, and output generation. +An API is a class with members named for each resource (or tree of resources). For example, an `apple` is a resource. You can request an `apple` over HTTP: -[^1]: Note that Presto avoids MVC by solving a smaller problem and relying on built in functionality for delegation and generating output. + GET fruit/apples/spartan.json?tags=large+red -How it works ------------- -Presto uses the best parts of PHP and Apache, as they're intended to be used. It relies on `.htaccess` for routing, and built-in PHP behaviours for delegation and class loading. The principle is that relying on existing, simple approaches results in less framework, fewer bugs, and less to learn that isn't useful elsewhere. +Presto maps the request to a file, class, and class member automatically. It loads the file, creates an instance of the class, and executes the member that best fits: -*Note: PHP 5.3 or better is requried, as Presto relies on anonymous functions, and other newer PHP features. This makes it possible to write very clean, simple web services.* -An example API --------------- - - class info - extends API { - - // Set up the API - public function __construct() { - parent::__construct(get_class()); - if (!self::isSignedIn()) throw new Exception('Auth required', 401); - } - - // GET info/time.json - gets the local machine time - public function get_time($ctx) { - $dom = (object) getdate(); - return $dom; // <-- returns the DOM as JSON + /* Loaded from 'fruit/apples.php' */ + + class apples extends API { + + public function get($params, $options, $body, $type) { + + if (count($params) === 0) + throw new Exception('Missing required parameter', 400); + + /* Exceptions are returned as valid HTTP/content type + responses */ + + $thing = (object) array( + 'name' => $params[0], + 'tags' => $options['tags'] + ); + + return $thing; // to client in requested format } - - // PUT info/time.json - public function put_time($ctx) { throw new Exception('Not implemented', 501); } - // POST info/time.json - public function post_time($ctx) { throw new Exception('Not implemented', 501); } } -The class promises a GET, PUT, and POST interface for time. All other requests will return a standard 404. The PUT and POST are not implemented yet, so they return a 501 (not implemented). -Presto maps requests to PHP files and objects, providing parameters and other calling information in the `$ctx` parameter. A request like: +The `$thing` is automatically converted by Presto to the requested `Content-Type`, either implied by the request or the appropriate HTTP header. For formats not supported by default, *Output Adapters* can be defined and registered, or the type can be passed through for resources that are not based DOM style data. - GET /info/time.json - -Maps to: +Any HTTP request type is mapped automatically. For example, you can request a list of `apple` types available from the API: - [info.php] info->get_time($ctx); - -The context includes the call parameters, the `content-type`, and request method. Handling of `content-type` is built in for simple cases, so that any data returned from an API is automatically transformed into the required type. For our simple example, returning the result of `datetime()` to the request produces: - - { - "seconds" => 40 - "minutes" => 58 - "hours" => 21 - "mday" => 17 - "wday" => 2 - "mon" => 6 - "year" => 2003 - "yday" => 167 - "weekday" => "Tuesday" - "month" => "June" - "0" => 1055901520 - } + LIST apples.json?colour=red + +A LIST request is mapped to a function of the same name: + + public function list(/* … */) { return array(); } -Advanced features ------------------ -There are a number of ways you can extend Presto. You can: +More complex resources are possible by either delegating (based on regex patterns), or by adding specific handlers. For example, you could add a `seeds` branch to the `apple` resource. Getting a list of seeds would map to: + + public function list_seeds(/* … */) { array(); } + +### What about errors? + +Errors are handled by the toolkit as standard PHP exceptions and standard PHP errors are remapped (where possible) to exceptions to ensure that logic and code errors are returned to clients as valid API returns. This means that individual APIs do not need to check return values, nor do they need to `try` and `catch` unless they need to do something special. Presto maps the standard exceptions to HTTP statuses and output in whatever format was requested where possible. + +For example, if you encounter a parameter error you can simply throw an exception: + + if (empty($param)) + throw new Exception('Missing required parameter', 400); + +Presto translates the exception into a `400` with an appropriately encoded body. + +The resulting API code is much more focused on carefully testing parameters, retrieving appropriate resources, and building rich DOMs instead of boilerplate code, managing responses, excessive error checking, routing, and other complex output generation. + + +Other interesting features +========================== + +You can also: * Add additional `content-types` by adding *output adapters* (`JSON`, simple `XML`, and simple `HTML` are built in) * Add `content-type` filters to define what types of payloads a given resource supports. * Add *custom delegation* for special resource types + +**Note that PHP 5.3 or better is required, as Presto relies on anonymous functions and other newer PHP features. This makes it possible to write very clean, simple web services.** diff --git a/TODO.taskpaper b/TODO.taskpaper deleted file mode 100644 index 454eb8a..0000000 --- a/TODO.taskpaper +++ /dev/null @@ -1,14 +0,0 @@ -Presto: - -- header exposure (nice, per type?) -- Wrap Simple SSO library in a more reusable package -Documentation: -- Quick tutorial -- FAQ - - multiple deligates - - limiting content types -- per file/function -Archive: - - automated examples @done - - New introduction @done - - A better example @done diff --git a/_tests/index.php b/_tests/index.php deleted file mode 120000 index 712d49c..0000000 --- a/_tests/index.php +++ /dev/null @@ -1 +0,0 @@ -run-all-tests.php \ No newline at end of file diff --git a/_tests/run-all-tests.php b/_tests/run-all-tests.php deleted file mode 100644 index bfbe139..0000000 --- a/_tests/run-all-tests.php +++ /dev/null @@ -1,187 +0,0 @@ -getCode()} - {$e->getMessage()}", 'FAIL'); - print("\n"); - print($e); - -} - -function response_complex_types_tests() { - test("Response - complex types tests"); - - $dom = << 'json' ) ); - $r->ok($dom); - print "\n"; - status("JSON output for complex data", 'OK'); - - $r = new Response((object) array( 'res' => 'htm' ) ); // htm intentional, tests match - $r->ok($dom); - print "\n"; - status("Default HTML output for complex data", 'OK'); - - unset($r); - $r = new Response((object) array( 'res' => 'htm' ) ); // htm intentional, tests type matching - - Response::add_type_handler('.*\/htm.*', _encode_html, function($k, &$n, &$a, $d) { - static $parents = array(); - static $was = 0; - - // track parents - - if ($was >= $d) // traversing up tree - for($i = 0; $i < ($was - $d) + 1; $i++) array_pop($parents); - - $parents[] = $k; - $p_at = count($parents) - 2; - $p = $p_at >= 0 ? $parents[$p_at] : ''; - - // remap node name - - switch ($k) { - case 'chapters': $k = 'div'; break; - case 'title': $k = 'h'.($d-1); break; - case 'ideas': $k = 'ul'; break; - case 'li': - if ($p === 'chapters') { // remap chapter LIs - $k = 'section'; - // also pull out the ID as an attribute - if (array_key_exists('id', $n)) { - $a .= " id='{$n['id']}'"; - unset($n['id']); - } - } - break; - } - - $was = $d; // track depth for parents path - - return $k; - } ); - - $r->ok($dom); - print "\n"; - status("Mapped HTML output for complex data", 'OK'); - - // TODO custom mapper / handler -} - -function response_tests() { - test("Response tests"); - $r = new Response((object) array( 'res' => 'json' ) ); - status("Created response object", 'OK'); - - $r->ok( array('test' => 'test data') ); - print "\n"; - status("Responded with simple JSON transform", 'OK'); - - $r = new Response((object) array( 'res' => 'html' ) ); - $r->ok( array('test' => 'test data') ); - print "\n"; - status("Responded with simple HTML transform", 'OK'); -} - -/* Test view helper(s) */ -function simple_view_tests() { - test("View tests"); - - View::$root = realpath(__DIR__) . '/'; - $v = new View('test-view', array('name' => 'test')); - status("Created simple view", 'OK'); - -} - -/* Test PDO and wrapper(s) */ -function database_tests() { - test("DB/PDO tests"); - - $dsn = 'mysql:host=localhost;dbname=test'; - $user = 'test'; - $password = '12345'; - $config = array( - PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8', - ); - - $db = new db($dsn, $user, $password, $config); - status("Connected to `$dsn`", 'OK'); - - $r = $db->select('SELECT * FROM Test'); - status("Simple select", 'OK'); - result($r); -} - -/* Test service class */ -function service_tests() { - test("Service tests"); - - $config = array( - 'service' => 'https://api.twitter.com/', - 'extra' => '1', - 'debug' => 0, - 'referer' => 'presto/test-script', - 'agent' => 'presto/1.0' - ); - - $service = new Service($config); - - status("Created test service", 'OK'); - //https://api.twitter.com/1/help/test.json - $data = $service->get_help('test.json'); - status("Simple GET request", 'OK'); - result($data); -} - - -function test($text) { - print "\n=[ $text ]========================================\n\n"; -} - -/* Display status (with some console highlighting) */ -function status($text, $status) { - $status = strtoupper($status); - $c = $status == 'OK' ? '42' : '41'; - - print " \033[1;{$c};30m[ {$status} ]\033[0m\t\t$text\n"; -} -/* Display detailed results */ -function result($t) { ?> - ----- - - - ----- - - - diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..53f4ebd --- /dev/null +++ b/composer.json @@ -0,0 +1,36 @@ +{ + "name": "napkinware/presto", + "version": "1.0.0", + "description": "A simple RESTful API library for PHP.", + "keywords": ["REST", "framework", "PHP"], + "homepage": "http://presto.napkinware.com", + "support": { + "issues": "https://github.com/robotpony/Presto/issues" + }, + "license": "MIT", + "authors": [ + { + "name": "Bruce Alderson", + "email": "bruce@napkinware.com" + } + ], + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/robotpony/presto" + } + ], + "minimum-stability": "stable", + "require": { + "php": ">=5.3.15", + "ext-curl": ">=0", + "ext-fileinfo": ">=1.0.0", + "ext-date": ">=5.3.0", + "ext-json": ">=1.2.1", + "lib-openssl": ">=0.9", + "lib-curl": ">=7.24.0" + }, + "require-dev": { + + } +} \ No newline at end of file diff --git a/docs/application-state.md b/docs/application-state.md index e69de29..be735af 100644 --- a/docs/application-state.md +++ b/docs/application-state.md @@ -0,0 +1,2 @@ +Application state (AKA, sessions) +--------------------------------- \ No newline at end of file diff --git a/docs/configuration.md b/docs/configuration.md index e69de29..b26e07e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -0,0 +1,2 @@ +Configuration +------------- \ No newline at end of file diff --git a/docs/faq.md b/docs/faq.md index 2a40c26..313aa55 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -1,7 +1,5 @@ Frequently asked things -======================= - -Document style: FAQ +----------------------- ## Q: Why no MVC? diff --git a/docs/favicon.ico b/docs/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/docs/favicon.png b/docs/favicon.png new file mode 100644 index 0000000..2cd5843 Binary files /dev/null and b/docs/favicon.png differ diff --git a/docs/generating-output.md b/docs/generating-output.md index 7b3becf..0c660bb 100644 --- a/docs/generating-output.md +++ b/docs/generating-output.md @@ -1,5 +1,5 @@ Generating output -================== +----------------- All Presto API functions return a simple DOM object. This object (or array) is translated into output using a type handler, which is selected by the requested `content-type`. diff --git a/docs/inc.php b/docs/inc.php new file mode 120000 index 0000000..028bea4 --- /dev/null +++ b/docs/inc.php @@ -0,0 +1 @@ +../lib/inc.php \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index c0cb9db..ca09874 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,9 +1,23 @@ -Presto for PHP - developer documentation -======================================== +Developer documentation +----------------------- +[License](/LICENSE.html) -Finding your way around ------------------------ +[Installation instructions](/INSTALL.html) + +[Frequently Asked Things (FAQ)](/docs/faq.html) + +[Application state](/docs/application-state.html) + +[Configuring Presto](/docs/configuration.html) + +[Output adapters](/docs/generating-output.html) + + +Missing topics +============== + +These are things that aren't quite covered in the documentation yet. * API classes (how requests are mapped to members) * Returning data to clients diff --git a/docs/index.php b/docs/index.php new file mode 100644 index 0000000..b2ca449 --- /dev/null +++ b/docs/index.php @@ -0,0 +1,106 @@ + + + + + + + + + + + +<?= $title ?> + + + + + + + + + + + +Fork me on GitHub + +
+

+ + + +
+ +
+
+ + +

Not found

+

The document you were looking for wasn't found.

+Installation error

Your installation is missing the markdown submodule. Please run git submodule init; git submodule update from the Presto root.

"; + else { + $text = Markdown(file_get_contents($file)); + $text = str_replace('
', '
', $text);
+	}
+?>
+
+
+	
+
+
+ + + + + + + + + + + + + diff --git a/docs/js/md.js b/docs/js/md.js new file mode 100644 index 0000000..e69de29 diff --git a/docs/lib/markdown/License.text b/docs/lib/markdown/License.text new file mode 100755 index 0000000..4d6bf8b --- /dev/null +++ b/docs/lib/markdown/License.text @@ -0,0 +1,36 @@ +PHP Markdown & Extra +Copyright (c) 2004-2009 Michel Fortin + +All rights reserved. + +Based on Markdown +Copyright (c) 2003-2006 John Gruber + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +* Neither the name "Markdown" nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +This software is provided by the copyright holders and contributors "as +is" and any express or implied warranties, including, but not limited +to, the implied warranties of merchantability and fitness for a +particular purpose are disclaimed. In no event shall the copyright owner +or contributors be liable for any direct, indirect, incidental, special, +exemplary, or consequential damages (including, but not limited to, +procurement of substitute goods or services; loss of use, data, or +profits; or business interruption) however caused and on any theory of +liability, whether in contract, strict liability, or tort (including +negligence or otherwise) arising in any way out of the use of this +software, even if advised of the possibility of such damage. diff --git a/docs/lib/markdown/markdown.php b/docs/lib/markdown/markdown.php new file mode 100755 index 0000000..f548fc2 --- /dev/null +++ b/docs/lib/markdown/markdown.php @@ -0,0 +1,2932 @@ + +# +# Original Markdown +# Copyright (c) 2004-2006 John Gruber +# +# + + +define( 'MARKDOWN_VERSION', "1.0.1o" ); # Sun 8 Jan 2012 +define( 'MARKDOWNEXTRA_VERSION', "1.2.5" ); # Sun 8 Jan 2012 + + +# +# Global default settings: +# + +# Change to ">" for HTML output +@define( 'MARKDOWN_EMPTY_ELEMENT_SUFFIX', " />"); + +# Define the width of a tab for code blocks. +@define( 'MARKDOWN_TAB_WIDTH', 4 ); + +# Optional title attribute for footnote links and backlinks. +@define( 'MARKDOWN_FN_LINK_TITLE', "" ); +@define( 'MARKDOWN_FN_BACKLINK_TITLE', "" ); + +# Optional class attribute for footnote links and backlinks. +@define( 'MARKDOWN_FN_LINK_CLASS', "" ); +@define( 'MARKDOWN_FN_BACKLINK_CLASS', "" ); + + +# +# WordPress settings: +# + +# Change to false to remove Markdown from posts and/or comments. +@define( 'MARKDOWN_WP_POSTS', true ); +@define( 'MARKDOWN_WP_COMMENTS', true ); + + + +### Standard Function Interface ### + +@define( 'MARKDOWN_PARSER_CLASS', 'MarkdownExtra_Parser' ); + +function Markdown($text) { +# +# Initialize the parser and return the result of its transform method. +# + # Setup static parser variable. + static $parser; + if (!isset($parser)) { + $parser_class = MARKDOWN_PARSER_CLASS; + $parser = new $parser_class; + } + + # Transform text using parser. + return $parser->transform($text); +} + + +### WordPress Plugin Interface ### + +/* +Plugin Name: Markdown Extra +Plugin URI: http://michelf.com/projects/php-markdown/ +Description: Markdown syntax allows you to write using an easy-to-read, easy-to-write plain text format. Based on the original Perl version by John Gruber. More... +Version: 1.2.5 +Author: Michel Fortin +Author URI: http://michelf.com/ +*/ + +if (isset($wp_version)) { + # More details about how it works here: + # + + # Post content and excerpts + # - Remove WordPress paragraph generator. + # - Run Markdown on excerpt, then remove all tags. + # - Add paragraph tag around the excerpt, but remove it for the excerpt rss. + if (MARKDOWN_WP_POSTS) { + remove_filter('the_content', 'wpautop'); + remove_filter('the_content_rss', 'wpautop'); + remove_filter('the_excerpt', 'wpautop'); + add_filter('the_content', 'mdwp_MarkdownPost', 6); + add_filter('the_content_rss', 'mdwp_MarkdownPost', 6); + add_filter('get_the_excerpt', 'mdwp_MarkdownPost', 6); + add_filter('get_the_excerpt', 'trim', 7); + add_filter('the_excerpt', 'mdwp_add_p'); + add_filter('the_excerpt_rss', 'mdwp_strip_p'); + + remove_filter('content_save_pre', 'balanceTags', 50); + remove_filter('excerpt_save_pre', 'balanceTags', 50); + add_filter('the_content', 'balanceTags', 50); + add_filter('get_the_excerpt', 'balanceTags', 9); + } + + # Add a footnote id prefix to posts when inside a loop. + function mdwp_MarkdownPost($text) { + static $parser; + if (!$parser) { + $parser_class = MARKDOWN_PARSER_CLASS; + $parser = new $parser_class; + } + if (is_single() || is_page() || is_feed()) { + $parser->fn_id_prefix = ""; + } else { + $parser->fn_id_prefix = get_the_ID() . "."; + } + return $parser->transform($text); + } + + # Comments + # - Remove WordPress paragraph generator. + # - Remove WordPress auto-link generator. + # - Scramble important tags before passing them to the kses filter. + # - Run Markdown on excerpt then remove paragraph tags. + if (MARKDOWN_WP_COMMENTS) { + remove_filter('comment_text', 'wpautop', 30); + remove_filter('comment_text', 'make_clickable'); + add_filter('pre_comment_content', 'Markdown', 6); + add_filter('pre_comment_content', 'mdwp_hide_tags', 8); + add_filter('pre_comment_content', 'mdwp_show_tags', 12); + add_filter('get_comment_text', 'Markdown', 6); + add_filter('get_comment_excerpt', 'Markdown', 6); + add_filter('get_comment_excerpt', 'mdwp_strip_p', 7); + + global $mdwp_hidden_tags, $mdwp_placeholders; + $mdwp_hidden_tags = explode(' ', + '

 
  • '); + $mdwp_placeholders = explode(' ', str_rot13( + 'pEj07ZbbBZ U1kqgh4w4p pre2zmeN6K QTi31t9pre ol0MP1jzJR '. + 'ML5IjmbRol ulANi1NsGY J7zRLJqPul liA8ctl16T K9nhooUHli')); + } + + function mdwp_add_p($text) { + if (!preg_match('{^$|^<(p|ul|ol|dl|pre|blockquote)>}i', $text)) { + $text = '

    '.$text.'

    '; + $text = preg_replace('{\n{2,}}', "

    \n\n

    ", $text); + } + return $text; + } + + function mdwp_strip_p($t) { return preg_replace('{}i', '', $t); } + + function mdwp_hide_tags($text) { + global $mdwp_hidden_tags, $mdwp_placeholders; + return str_replace($mdwp_hidden_tags, $mdwp_placeholders, $text); + } + function mdwp_show_tags($text) { + global $mdwp_hidden_tags, $mdwp_placeholders; + return str_replace($mdwp_placeholders, $mdwp_hidden_tags, $text); + } +} + + +### bBlog Plugin Info ### + +function identify_modifier_markdown() { + return array( + 'name' => 'markdown', + 'type' => 'modifier', + 'nicename' => 'PHP Markdown Extra', + 'description' => 'A text-to-HTML conversion tool for web writers', + 'authors' => 'Michel Fortin and John Gruber', + 'licence' => 'GPL', + 'version' => MARKDOWNEXTRA_VERSION, + 'help' => 'Markdown syntax allows you to write using an easy-to-read, easy-to-write plain text format. Based on the original Perl version by John Gruber. More...', + ); +} + + +### Smarty Modifier Interface ### + +function smarty_modifier_markdown($text) { + return Markdown($text); +} + + +### Textile Compatibility Mode ### + +# Rename this file to "classTextile.php" and it can replace Textile everywhere. + +if (strcasecmp(substr(__FILE__, -16), "classTextile.php") == 0) { + # Try to include PHP SmartyPants. Should be in the same directory. + @include_once 'smartypants.php'; + # Fake Textile class. It calls Markdown instead. + class Textile { + function TextileThis($text, $lite='', $encode='') { + if ($lite == '' && $encode == '') $text = Markdown($text); + if (function_exists('SmartyPants')) $text = SmartyPants($text); + return $text; + } + # Fake restricted version: restrictions are not supported for now. + function TextileRestricted($text, $lite='', $noimage='') { + return $this->TextileThis($text, $lite); + } + # Workaround to ensure compatibility with TextPattern 4.0.3. + function blockLite($text) { return $text; } + } +} + + + +# +# Markdown Parser Class +# + +class Markdown_Parser { + + # Regex to match balanced [brackets]. + # Needed to insert a maximum bracked depth while converting to PHP. + var $nested_brackets_depth = 6; + var $nested_brackets_re; + + var $nested_url_parenthesis_depth = 4; + var $nested_url_parenthesis_re; + + # Table of hash values for escaped characters: + var $escape_chars = '\`*_{}[]()>#+-.!'; + var $escape_chars_re; + + # Change to ">" for HTML output. + var $empty_element_suffix = MARKDOWN_EMPTY_ELEMENT_SUFFIX; + var $tab_width = MARKDOWN_TAB_WIDTH; + + # Change to `true` to disallow markup or entities. + var $no_markup = false; + var $no_entities = false; + + # Predefined urls and titles for reference links and images. + var $predef_urls = array(); + var $predef_titles = array(); + + + function Markdown_Parser() { + # + # Constructor function. Initialize appropriate member variables. + # + $this->_initDetab(); + $this->prepareItalicsAndBold(); + + $this->nested_brackets_re = + str_repeat('(?>[^\[\]]+|\[', $this->nested_brackets_depth). + str_repeat('\])*', $this->nested_brackets_depth); + + $this->nested_url_parenthesis_re = + str_repeat('(?>[^()\s]+|\(', $this->nested_url_parenthesis_depth). + str_repeat('(?>\)))*', $this->nested_url_parenthesis_depth); + + $this->escape_chars_re = '['.preg_quote($this->escape_chars).']'; + + # Sort document, block, and span gamut in ascendent priority order. + asort($this->document_gamut); + asort($this->block_gamut); + asort($this->span_gamut); + } + + + # Internal hashes used during transformation. + var $urls = array(); + var $titles = array(); + var $html_hashes = array(); + + # Status flag to avoid invalid nesting. + var $in_anchor = false; + + + function setup() { + # + # Called before the transformation process starts to setup parser + # states. + # + # Clear global hashes. + $this->urls = $this->predef_urls; + $this->titles = $this->predef_titles; + $this->html_hashes = array(); + + $in_anchor = false; + } + + function teardown() { + # + # Called after the transformation process to clear any variable + # which may be taking up memory unnecessarly. + # + $this->urls = array(); + $this->titles = array(); + $this->html_hashes = array(); + } + + + function transform($text) { + # + # Main function. Performs some preprocessing on the input text + # and pass it through the document gamut. + # + $this->setup(); + + # Remove UTF-8 BOM and marker character in input, if present. + $text = preg_replace('{^\xEF\xBB\xBF|\x1A}', '', $text); + + # Standardize line endings: + # DOS to Unix and Mac to Unix + $text = preg_replace('{\r\n?}', "\n", $text); + + # Make sure $text ends with a couple of newlines: + $text .= "\n\n"; + + # Convert all tabs to spaces. + $text = $this->detab($text); + + # Turn block-level HTML blocks into hash entries + $text = $this->hashHTMLBlocks($text); + + # Strip any lines consisting only of spaces and tabs. + # This makes subsequent regexen easier to write, because we can + # match consecutive blank lines with /\n+/ instead of something + # contorted like /[ ]*\n+/ . + $text = preg_replace('/^[ ]+$/m', '', $text); + + # Run document gamut methods. + foreach ($this->document_gamut as $method => $priority) { + $text = $this->$method($text); + } + + $this->teardown(); + + return $text . "\n"; + } + + var $document_gamut = array( + # Strip link definitions, store in hashes. + "stripLinkDefinitions" => 20, + + "runBasicBlockGamut" => 30, + ); + + + function stripLinkDefinitions($text) { + # + # Strips link definitions from text, stores the URLs and titles in + # hash references. + # + $less_than_tab = $this->tab_width - 1; + + # Link defs are in the form: ^[id]: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1 + [ ]* + \n? # maybe *one* newline + [ ]* + (?: + <(.+?)> # url = $2 + | + (\S+?) # url = $3 + ) + [ ]* + \n? # maybe one newline + [ ]* + (?: + (?<=\s) # lookbehind for whitespace + ["(] + (.*?) # title = $4 + [")] + [ ]* + )? # title is optional + (?:\n+|\Z) + }xm', + array(&$this, '_stripLinkDefinitions_callback'), + $text); + return $text; + } + function _stripLinkDefinitions_callback($matches) { + $link_id = strtolower($matches[1]); + $url = $matches[2] == '' ? $matches[3] : $matches[2]; + $this->urls[$link_id] = $url; + $this->titles[$link_id] =& $matches[4]; + return ''; # String that will replace the block + } + + + function hashHTMLBlocks($text) { + if ($this->no_markup) return $text; + + $less_than_tab = $this->tab_width - 1; + + # Hashify HTML blocks: + # We only want to do this for block-level HTML tags, such as headers, + # lists, and tables. That's because we still want to wrap

    s around + # "paragraphs" that are wrapped in non-block-level tags, such as anchors, + # phrase emphasis, and spans. The list of tags we're looking for is + # hard-coded: + # + # * List "a" is made of tags which can be both inline or block-level. + # These will be treated block-level when the start tag is alone on + # its line, otherwise they're not matched here and will be taken as + # inline later. + # * List "b" is made of tags which are always block-level; + # + $block_tags_a_re = 'ins|del'; + $block_tags_b_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|'. + 'script|noscript|form|fieldset|iframe|math'; + + # Regular expression for the content of a block tag. + $nested_tags_level = 4; + $attr = ' + (?> # optional tag attributes + \s # starts with whitespace + (?> + [^>"/]+ # text outside quotes + | + /+(?!>) # slash not followed by ">" + | + "[^"]*" # text inside double quotes (tolerate ">") + | + \'[^\']*\' # text inside single quotes (tolerate ">") + )* + )? + '; + $content = + str_repeat(' + (?> + [^<]+ # content without tag + | + <\2 # nested opening tag + '.$attr.' # attributes + (?> + /> + | + >', $nested_tags_level). # end of opening tag + '.*?'. # last level nested tag content + str_repeat(' + # closing nested tag + ) + | + <(?!/\2\s*> # other tags with a different name + ) + )*', + $nested_tags_level); + $content2 = str_replace('\2', '\3', $content); + + # First, look for nested blocks, e.g.: + #

    + #
    + # tags for inner block must be indented. + #
    + #
    + # + # The outermost tags must start at the left margin for this to match, and + # the inner nested divs must be indented. + # We need to do this before the next, more liberal match, because the next + # match will start at the first `
    ` and stop at the first `
    `. + $text = preg_replace_callback('{(?> + (?> + (?<=\n\n) # Starting after a blank line + | # or + \A\n? # the beginning of the doc + ) + ( # save in $1 + + # Match from `\n` to `\n`, handling nested tags + # in between. + + [ ]{0,'.$less_than_tab.'} + <('.$block_tags_b_re.')# start tag = $2 + '.$attr.'> # attributes followed by > and \n + '.$content.' # content, support nesting + # the matching end tag + [ ]* # trailing spaces/tabs + (?=\n+|\Z) # followed by a newline or end of document + + | # Special version for tags of group a. + + [ ]{0,'.$less_than_tab.'} + <('.$block_tags_a_re.')# start tag = $3 + '.$attr.'>[ ]*\n # attributes followed by > + '.$content2.' # content, support nesting + # the matching end tag + [ ]* # trailing spaces/tabs + (?=\n+|\Z) # followed by a newline or end of document + + | # Special case just for
    . It was easier to make a special + # case than to make the other regex more complicated. + + [ ]{0,'.$less_than_tab.'} + <(hr) # start tag = $2 + '.$attr.' # attributes + /?> # the matching end tag + [ ]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + + | # Special case for standalone HTML comments: + + [ ]{0,'.$less_than_tab.'} + (?s: + + ) + [ ]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + + | # PHP and ASP-style processor instructions ( + ) + [ ]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + + ) + )}Sxmi', + array(&$this, '_hashHTMLBlocks_callback'), + $text); + + return $text; + } + function _hashHTMLBlocks_callback($matches) { + $text = $matches[1]; + $key = $this->hashBlock($text); + return "\n\n$key\n\n"; + } + + + function hashPart($text, $boundary = 'X') { + # + # Called whenever a tag must be hashed when a function insert an atomic + # element in the text stream. Passing $text to through this function gives + # a unique text-token which will be reverted back when calling unhash. + # + # The $boundary argument specify what character should be used to surround + # the token. By convension, "B" is used for block elements that needs not + # to be wrapped into paragraph tags at the end, ":" is used for elements + # that are word separators and "X" is used in the general case. + # + # Swap back any tag hash found in $text so we do not have to `unhash` + # multiple times at the end. + $text = $this->unhash($text); + + # Then hash the block. + static $i = 0; + $key = "$boundary\x1A" . ++$i . $boundary; + $this->html_hashes[$key] = $text; + return $key; # String that will replace the tag. + } + + + function hashBlock($text) { + # + # Shortcut function for hashPart with block-level boundaries. + # + return $this->hashPart($text, 'B'); + } + + + var $block_gamut = array( + # + # These are all the transformations that form block-level + # tags like paragraphs, headers, and list items. + # + "doHeaders" => 10, + "doHorizontalRules" => 20, + + "doLists" => 40, + "doCodeBlocks" => 50, + "doBlockQuotes" => 60, + ); + + function runBlockGamut($text) { + # + # Run block gamut tranformations. + # + # We need to escape raw HTML in Markdown source before doing anything + # else. This need to be done for each block, and not only at the + # begining in the Markdown function since hashed blocks can be part of + # list items and could have been indented. Indented blocks would have + # been seen as a code block in a previous pass of hashHTMLBlocks. + $text = $this->hashHTMLBlocks($text); + + return $this->runBasicBlockGamut($text); + } + + function runBasicBlockGamut($text) { + # + # Run block gamut tranformations, without hashing HTML blocks. This is + # useful when HTML blocks are known to be already hashed, like in the first + # whole-document pass. + # + foreach ($this->block_gamut as $method => $priority) { + $text = $this->$method($text); + } + + # Finally form paragraph and restore hashed blocks. + $text = $this->formParagraphs($text); + + return $text; + } + + + function doHorizontalRules($text) { + # Do Horizontal Rules: + return preg_replace( + '{ + ^[ ]{0,3} # Leading space + ([-*_]) # $1: First marker + (?> # Repeated marker group + [ ]{0,2} # Zero, one, or two spaces. + \1 # Marker character + ){2,} # Group repeated at least twice + [ ]* # Tailing spaces + $ # End of line. + }mx', + "\n".$this->hashBlock("empty_element_suffix")."\n", + $text); + } + + + var $span_gamut = array( + # + # These are all the transformations that occur *within* block-level + # tags like paragraphs, headers, and list items. + # + # Process character escapes, code spans, and inline HTML + # in one shot. + "parseSpan" => -30, + + # Process anchor and image tags. Images must come first, + # because ![foo][f] looks like an anchor. + "doImages" => 10, + "doAnchors" => 20, + + # Make links out of things like `` + # Must come after doAnchors, because you can use < and > + # delimiters in inline links like [this](). + "doAutoLinks" => 30, + "encodeAmpsAndAngles" => 40, + + "doItalicsAndBold" => 50, + "doHardBreaks" => 60, + ); + + function runSpanGamut($text) { + # + # Run span gamut tranformations. + # + foreach ($this->span_gamut as $method => $priority) { + $text = $this->$method($text); + } + + return $text; + } + + + function doHardBreaks($text) { + # Do hard breaks: + return preg_replace_callback('/ {2,}\n/', + array(&$this, '_doHardBreaks_callback'), $text); + } + function _doHardBreaks_callback($matches) { + return $this->hashPart("empty_element_suffix\n"); + } + + + function doAnchors($text) { + # + # Turn Markdown link shortcuts into XHTML tags. + # + if ($this->in_anchor) return $text; + $this->in_anchor = true; + + # + # First, handle reference-style links: [link text] [id] + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ('.$this->nested_brackets_re.') # link text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + ) + }xs', + array(&$this, '_doAnchors_reference_callback'), $text); + + # + # Next, inline-style links: [link text](url "optional title") + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ('.$this->nested_brackets_re.') # link text = $2 + \] + \( # literal paren + [ \n]* + (?: + <(.+?)> # href = $3 + | + ('.$this->nested_url_parenthesis_re.') # href = $4 + ) + [ \n]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # Title = $7 + \6 # matching quote + [ \n]* # ignore any spaces/tabs between closing quote and ) + )? # title is optional + \) + ) + }xs', + array(&$this, '_doAnchors_inline_callback'), $text); + + # + # Last, handle reference-style shortcuts: [link text] + # These must come last in case you've also got [link text][1] + # or [link text](/foo) + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ([^\[\]]+) # link text = $2; can\'t contain [ or ] + \] + ) + }xs', + array(&$this, '_doAnchors_reference_callback'), $text); + + $this->in_anchor = false; + return $text; + } + function _doAnchors_reference_callback($matches) { + $whole_match = $matches[1]; + $link_text = $matches[2]; + $link_id =& $matches[3]; + + if ($link_id == "") { + # for shortcut links like [this][] or [this]. + $link_id = $link_text; + } + + # lower-case and turn embedded newlines into spaces + $link_id = strtolower($link_id); + $link_id = preg_replace('{[ ]?\n}', ' ', $link_id); + + if (isset($this->urls[$link_id])) { + $url = $this->urls[$link_id]; + $url = $this->encodeAttribute($url); + + $result = "titles[$link_id] ) ) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text"; + $result = $this->hashPart($result); + } + else { + $result = $whole_match; + } + return $result; + } + function _doAnchors_inline_callback($matches) { + $whole_match = $matches[1]; + $link_text = $this->runSpanGamut($matches[2]); + $url = $matches[3] == '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + + $url = $this->encodeAttribute($url); + + $result = "encodeAttribute($title); + $result .= " title=\"$title\""; + } + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text"; + + return $this->hashPart($result); + } + + + function doImages($text) { + # + # Turn Markdown image shortcuts into tags. + # + # + # First, handle reference-style labeled images: ![alt text][id] + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + ('.$this->nested_brackets_re.') # alt text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + + ) + }xs', + array(&$this, '_doImages_reference_callback'), $text); + + # + # Next, handle inline images: ![alt text](url "optional title") + # Don't forget: encode * and _ + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + ('.$this->nested_brackets_re.') # alt text = $2 + \] + \s? # One optional whitespace character + \( # literal paren + [ \n]* + (?: + <(\S*)> # src url = $3 + | + ('.$this->nested_url_parenthesis_re.') # src url = $4 + ) + [ \n]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # title = $7 + \6 # matching quote + [ \n]* + )? # title is optional + \) + ) + }xs', + array(&$this, '_doImages_inline_callback'), $text); + + return $text; + } + function _doImages_reference_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $link_id = strtolower($matches[3]); + + if ($link_id == "") { + $link_id = strtolower($alt_text); # for shortcut links like ![this][]. + } + + $alt_text = $this->encodeAttribute($alt_text); + if (isset($this->urls[$link_id])) { + $url = $this->encodeAttribute($this->urls[$link_id]); + $result = "\"$alt_text\"";titles[$link_id])) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + $result .= $this->empty_element_suffix; + $result = $this->hashPart($result); + } + else { + # If there's no such link ID, leave intact: + $result = $whole_match; + } + + return $result; + } + function _doImages_inline_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $url = $matches[3] == '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + + $alt_text = $this->encodeAttribute($alt_text); + $url = $this->encodeAttribute($url); + $result = "\"$alt_text\"";encodeAttribute($title); + $result .= " title=\"$title\""; # $title already quoted + } + $result .= $this->empty_element_suffix; + + return $this->hashPart($result); + } + + + function doHeaders($text) { + # Setext-style headers: + # Header 1 + # ======== + # + # Header 2 + # -------- + # + $text = preg_replace_callback('{ ^(.+?)[ ]*\n(=+|-+)[ ]*\n+ }mx', + array(&$this, '_doHeaders_callback_setext'), $text); + + # atx-style headers: + # # Header 1 + # ## Header 2 + # ## Header 2 with closing hashes ## + # ... + # ###### Header 6 + # + $text = preg_replace_callback('{ + ^(\#{1,6}) # $1 = string of #\'s + [ ]* + (.+?) # $2 = Header text + [ ]* + \#* # optional closing #\'s (not counted) + \n+ + }xm', + array(&$this, '_doHeaders_callback_atx'), $text); + + return $text; + } + function _doHeaders_callback_setext($matches) { + # Terrible hack to check we haven't found an empty list item. + if ($matches[2] == '-' && preg_match('{^-(?: |$)}', $matches[1])) + return $matches[0]; + + $level = $matches[2]{0} == '=' ? 1 : 2; + $block = "".$this->runSpanGamut($matches[1]).""; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + function _doHeaders_callback_atx($matches) { + $level = strlen($matches[1]); + $block = "".$this->runSpanGamut($matches[2]).""; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + + + function doLists($text) { + # + # Form HTML ordered (numbered) and unordered (bulleted) lists. + # + $less_than_tab = $this->tab_width - 1; + + # Re-usable patterns to match list item bullets and number markers: + $marker_ul_re = '[*+-]'; + $marker_ol_re = '\d+[\.]'; + $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)"; + + $markers_relist = array( + $marker_ul_re => $marker_ol_re, + $marker_ol_re => $marker_ul_re, + ); + + foreach ($markers_relist as $marker_re => $other_marker_re) { + # Re-usable pattern to match any entirel ul or ol list: + $whole_list_re = ' + ( # $1 = whole list + ( # $2 + ([ ]{0,'.$less_than_tab.'}) # $3 = number of spaces + ('.$marker_re.') # $4 = first list item marker + [ ]+ + ) + (?s:.+?) + ( # $5 + \z + | + \n{2,} + (?=\S) + (?! # Negative lookahead for another list item marker + [ ]* + '.$marker_re.'[ ]+ + ) + | + (?= # Lookahead for another kind of list + \n + \3 # Must have the same indentation + '.$other_marker_re.'[ ]+ + ) + ) + ) + '; // mx + + # We use a different prefix before nested lists than top-level lists. + # See extended comment in _ProcessListItems(). + + if ($this->list_level) { + $text = preg_replace_callback('{ + ^ + '.$whole_list_re.' + }mx', + array(&$this, '_doLists_callback'), $text); + } + else { + $text = preg_replace_callback('{ + (?:(?<=\n)\n|\A\n?) # Must eat the newline + '.$whole_list_re.' + }mx', + array(&$this, '_doLists_callback'), $text); + } + } + + return $text; + } + function _doLists_callback($matches) { + # Re-usable patterns to match list item bullets and number markers: + $marker_ul_re = '[*+-]'; + $marker_ol_re = '\d+[\.]'; + $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)"; + + $list = $matches[1]; + $list_type = preg_match("/$marker_ul_re/", $matches[4]) ? "ul" : "ol"; + + $marker_any_re = ( $list_type == "ul" ? $marker_ul_re : $marker_ol_re ); + + $list .= "\n"; + $result = $this->processListItems($list, $marker_any_re); + + $result = $this->hashBlock("<$list_type>\n" . $result . ""); + return "\n". $result ."\n\n"; + } + + var $list_level = 0; + + function processListItems($list_str, $marker_any_re) { + # + # Process the contents of a single ordered or unordered list, splitting it + # into individual list items. + # + # The $this->list_level global keeps track of when we're inside a list. + # Each time we enter a list, we increment it; when we leave a list, + # we decrement. If it's zero, we're not in a list anymore. + # + # We do this because when we're not inside a list, we want to treat + # something like this: + # + # I recommend upgrading to version + # 8. Oops, now this line is treated + # as a sub-list. + # + # As a single paragraph, despite the fact that the second line starts + # with a digit-period-space sequence. + # + # Whereas when we're inside a list (or sub-list), that line will be + # treated as the start of a sub-list. What a kludge, huh? This is + # an aspect of Markdown's syntax that's hard to parse perfectly + # without resorting to mind-reading. Perhaps the solution is to + # change the syntax rules such that sub-lists must start with a + # starting cardinal number; e.g. "1." or "a.". + + $this->list_level++; + + # trim trailing blank lines: + $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str); + + $list_str = preg_replace_callback('{ + (\n)? # leading line = $1 + (^[ ]*) # leading whitespace = $2 + ('.$marker_any_re.' # list marker and space = $3 + (?:[ ]+|(?=\n)) # space only required if item is not empty + ) + ((?s:.*?)) # list item text = $4 + (?:(\n+(?=\n))|\n) # tailing blank line = $5 + (?= \n* (\z | \2 ('.$marker_any_re.') (?:[ ]+|(?=\n)))) + }xm', + array(&$this, '_processListItems_callback'), $list_str); + + $this->list_level--; + return $list_str; + } + function _processListItems_callback($matches) { + $item = $matches[4]; + $leading_line =& $matches[1]; + $leading_space =& $matches[2]; + $marker_space = $matches[3]; + $tailing_blank_line =& $matches[5]; + + if ($leading_line || $tailing_blank_line || + preg_match('/\n{2,}/', $item)) + { + # Replace marker with the appropriate whitespace indentation + $item = $leading_space . str_repeat(' ', strlen($marker_space)) . $item; + $item = $this->runBlockGamut($this->outdent($item)."\n"); + } + else { + # Recursion for sub-lists: + $item = $this->doLists($this->outdent($item)); + $item = preg_replace('/\n+$/', '', $item); + $item = $this->runSpanGamut($item); + } + + return "
  • " . $item . "
  • \n"; + } + + + function doCodeBlocks($text) { + # + # Process Markdown `
    ` blocks.
    +	#
    +		$text = preg_replace_callback('{
    +				(?:\n\n|\A\n?)
    +				(	            # $1 = the code block -- one or more lines, starting with a space/tab
    +				  (?>
    +					[ ]{'.$this->tab_width.'}  # Lines must start with a tab or a tab-width of spaces
    +					.*\n+
    +				  )+
    +				)
    +				((?=^[ ]{0,'.$this->tab_width.'}\S)|\Z)	# Lookahead for non-space at line-start, or end of doc
    +			}xm',
    +			array(&$this, '_doCodeBlocks_callback'), $text);
    +
    +		return $text;
    +	}
    +	function _doCodeBlocks_callback($matches) {
    +		$codeblock = $matches[1];
    +
    +		$codeblock = $this->outdent($codeblock);
    +		$codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES);
    +
    +		# trim leading newlines and trailing newlines
    +		$codeblock = preg_replace('/\A\n+|\n+\z/', '', $codeblock);
    +
    +		$codeblock = "
    $codeblock\n
    "; + return "\n\n".$this->hashBlock($codeblock)."\n\n"; + } + + + function makeCodeSpan($code) { + # + # Create a code span markup for $code. Called from handleSpanToken. + # + $code = htmlspecialchars(trim($code), ENT_NOQUOTES); + return $this->hashPart("$code"); + } + + + var $em_relist = array( + '' => '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(?em_relist as $em => $em_re) { + foreach ($this->strong_relist as $strong => $strong_re) { + # Construct list of allowed token expressions. + $token_relist = array(); + if (isset($this->em_strong_relist["$em$strong"])) { + $token_relist[] = $this->em_strong_relist["$em$strong"]; + } + $token_relist[] = $em_re; + $token_relist[] = $strong_re; + + # Construct master expression from list. + $token_re = '{('. implode('|', $token_relist) .')}'; + $this->em_strong_prepared_relist["$em$strong"] = $token_re; + } + } + } + + function doItalicsAndBold($text) { + $token_stack = array(''); + $text_stack = array(''); + $em = ''; + $strong = ''; + $tree_char_em = false; + + while (1) { + # + # Get prepared regular expression for seraching emphasis tokens + # in current context. + # + $token_re = $this->em_strong_prepared_relist["$em$strong"]; + + # + # Each loop iteration search for the next emphasis token. + # Each token is then passed to handleSpanToken. + # + $parts = preg_split($token_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE); + $text_stack[0] .= $parts[0]; + $token =& $parts[1]; + $text =& $parts[2]; + + if (empty($token)) { + # Reached end of text span: empty stack without emitting. + # any more emphasis. + while ($token_stack[0]) { + $text_stack[1] .= array_shift($token_stack); + $text_stack[0] .= array_shift($text_stack); + } + break; + } + + $token_len = strlen($token); + if ($tree_char_em) { + # Reached closing marker while inside a three-char emphasis. + if ($token_len == 3) { + # Three-char closing marker, close em and strong. + array_shift($token_stack); + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "$span"; + $text_stack[0] .= $this->hashPart($span); + $em = ''; + $strong = ''; + } else { + # Other closing marker: close one em or strong and + # change current token state to match the other + $token_stack[0] = str_repeat($token{0}, 3-$token_len); + $tag = $token_len == 2 ? "strong" : "em"; + $span = $text_stack[0]; + $span = $this->runSpanGamut($span); + $span = "<$tag>$span"; + $text_stack[0] = $this->hashPart($span); + $$tag = ''; # $$tag stands for $em or $strong + } + $tree_char_em = false; + } else if ($token_len == 3) { + if ($em) { + # Reached closing marker for both em and strong. + # Closing strong marker: + for ($i = 0; $i < 2; ++$i) { + $shifted_token = array_shift($token_stack); + $tag = strlen($shifted_token) == 2 ? "strong" : "em"; + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "<$tag>$span"; + $text_stack[0] .= $this->hashPart($span); + $$tag = ''; # $$tag stands for $em or $strong + } + } else { + # Reached opening three-char emphasis marker. Push on token + # stack; will be handled by the special condition above. + $em = $token{0}; + $strong = "$em$em"; + array_unshift($token_stack, $token); + array_unshift($text_stack, ''); + $tree_char_em = true; + } + } else if ($token_len == 2) { + if ($strong) { + # Unwind any dangling emphasis marker: + if (strlen($token_stack[0]) == 1) { + $text_stack[1] .= array_shift($token_stack); + $text_stack[0] .= array_shift($text_stack); + } + # Closing strong marker: + array_shift($token_stack); + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "$span"; + $text_stack[0] .= $this->hashPart($span); + $strong = ''; + } else { + array_unshift($token_stack, $token); + array_unshift($text_stack, ''); + $strong = $token; + } + } else { + # Here $token_len == 1 + if ($em) { + if (strlen($token_stack[0]) == 1) { + # Closing emphasis marker: + array_shift($token_stack); + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "$span"; + $text_stack[0] .= $this->hashPart($span); + $em = ''; + } else { + $text_stack[0] .= $token; + } + } else { + array_unshift($token_stack, $token); + array_unshift($text_stack, ''); + $em = $token; + } + } + } + return $text_stack[0]; + } + + + function doBlockQuotes($text) { + $text = preg_replace_callback('/ + ( # Wrap whole match in $1 + (?> + ^[ ]*>[ ]? # ">" at the start of a line + .+\n # rest of the first line + (.+\n)* # subsequent consecutive lines + \n* # blanks + )+ + ) + /xm', + array(&$this, '_doBlockQuotes_callback'), $text); + + return $text; + } + function _doBlockQuotes_callback($matches) { + $bq = $matches[1]; + # trim one level of quoting - trim whitespace-only lines + $bq = preg_replace('/^[ ]*>[ ]?|^[ ]+$/m', '', $bq); + $bq = $this->runBlockGamut($bq); # recurse + + $bq = preg_replace('/^/m', " ", $bq); + # These leading spaces cause problem with
     content, 
    +		# so we need to fix that:
    +		$bq = preg_replace_callback('{(\s*
    .+?
    )}sx', + array(&$this, '_doBlockQuotes_callback2'), $bq); + + return "\n". $this->hashBlock("
    \n$bq\n
    ")."\n\n"; + } + function _doBlockQuotes_callback2($matches) { + $pre = $matches[1]; + $pre = preg_replace('/^ /m', '', $pre); + return $pre; + } + + + function formParagraphs($text) { + # + # Params: + # $text - string to process with html

    tags + # + # Strip leading and trailing lines: + $text = preg_replace('/\A\n+|\n+\z/', '', $text); + + $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY); + + # + # Wrap

    tags and unhashify HTML blocks + # + foreach ($grafs as $key => $value) { + if (!preg_match('/^B\x1A[0-9]+B$/', $value)) { + # Is a paragraph. + $value = $this->runSpanGamut($value); + $value = preg_replace('/^([ ]*)/', "

    ", $value); + $value .= "

    "; + $grafs[$key] = $this->unhash($value); + } + else { + # Is a block. + # Modify elements of @grafs in-place... + $graf = $value; + $block = $this->html_hashes[$graf]; + $graf = $block; +// if (preg_match('{ +// \A +// ( # $1 =
    tag +//
    ]* +// \b +// markdown\s*=\s* ([\'"]) # $2 = attr quote char +// 1 +// \2 +// [^>]* +// > +// ) +// ( # $3 = contents +// .* +// ) +// (
    ) # $4 = closing tag +// \z +// }xs', $block, $matches)) +// { +// list(, $div_open, , $div_content, $div_close) = $matches; +// +// # We can't call Markdown(), because that resets the hash; +// # that initialization code should be pulled into its own sub, though. +// $div_content = $this->hashHTMLBlocks($div_content); +// +// # Run document gamut methods on the content. +// foreach ($this->document_gamut as $method => $priority) { +// $div_content = $this->$method($div_content); +// } +// +// $div_open = preg_replace( +// '{\smarkdown\s*=\s*([\'"]).+?\1}', '', $div_open); +// +// $graf = $div_open . "\n" . $div_content . "\n" . $div_close; +// } + $grafs[$key] = $graf; + } + } + + return implode("\n\n", $grafs); + } + + + function encodeAttribute($text) { + # + # Encode text for a double-quoted HTML attribute. This function + # is *not* suitable for attributes enclosed in single quotes. + # + $text = $this->encodeAmpsAndAngles($text); + $text = str_replace('"', '"', $text); + return $text; + } + + + function encodeAmpsAndAngles($text) { + # + # Smart processing for ampersands and angle brackets that need to + # be encoded. Valid character entities are left alone unless the + # no-entities mode is set. + # + if ($this->no_entities) { + $text = str_replace('&', '&', $text); + } else { + # Ampersand-encoding based entirely on Nat Irons's Amputator + # MT plugin: + $text = preg_replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/', + '&', $text);; + } + # Encode remaining <'s + $text = str_replace('<', '<', $text); + + return $text; + } + + + function doAutoLinks($text) { + $text = preg_replace_callback('{<((https?|ftp|dict):[^\'">\s]+)>}i', + array(&$this, '_doAutoLinks_url_callback'), $text); + + # Email addresses: + $text = preg_replace_callback('{ + < + (?:mailto:)? + ( + (?: + [-!#$%&\'*+/=?^_`.{|}~\w\x80-\xFF]+ + | + ".*?" + ) + \@ + (?: + [-a-z0-9\x80-\xFF]+(\.[-a-z0-9\x80-\xFF]+)*\.[a-z]+ + | + \[[\d.a-fA-F:]+\] # IPv4 & IPv6 + ) + ) + > + }xi', + array(&$this, '_doAutoLinks_email_callback'), $text); + + return $text; + } + function _doAutoLinks_url_callback($matches) { + $url = $this->encodeAttribute($matches[1]); + $link = "$url"; + return $this->hashPart($link); + } + function _doAutoLinks_email_callback($matches) { + $address = $matches[1]; + $link = $this->encodeEmailAddress($address); + return $this->hashPart($link); + } + + + function encodeEmailAddress($addr) { + # + # Input: an email address, e.g. "foo@example.com" + # + # Output: the email address as a mailto link, with each character + # of the address encoded as either a decimal or hex entity, in + # the hopes of foiling most address harvesting spam bots. E.g.: + # + #

    foo@exampl + # e.com

    + # + # Based by a filter by Matthew Wickline, posted to BBEdit-Talk. + # With some optimizations by Milian Wolff. + # + $addr = "mailto:" . $addr; + $chars = preg_split('/(? $char) { + $ord = ord($char); + # Ignore non-ascii chars. + if ($ord < 128) { + $r = ($seed * (1 + $key)) % 100; # Pseudo-random function. + # roughly 10% raw, 45% hex, 45% dec + # '@' *must* be encoded. I insist. + if ($r > 90 && $char != '@') /* do nothing */; + else if ($r < 45) $chars[$key] = '&#x'.dechex($ord).';'; + else $chars[$key] = '&#'.$ord.';'; + } + } + + $addr = implode('', $chars); + $text = implode('', array_slice($chars, 7)); # text without `mailto:` + $addr = "$text"; + + return $addr; + } + + + function parseSpan($str) { + # + # Take the string $str and parse it into tokens, hashing embeded HTML, + # escaped characters and handling code spans. + # + $output = ''; + + $span_re = '{ + ( + \\\\'.$this->escape_chars_re.' + | + (?no_markup ? '' : ' + | + # comment + | + <\?.*?\?> | <%.*?%> # processing instruction + | + <[/!$]?[-a-zA-Z0-9:_]+ # regular tags + (?> + \s + (?>[^"\'>]+|"[^"]*"|\'[^\']*\')* + )? + > + ').' + ) + }xs'; + + while (1) { + # + # Each loop iteration seach for either the next tag, the next + # openning code span marker, or the next escaped character. + # Each token is then passed to handleSpanToken. + # + $parts = preg_split($span_re, $str, 2, PREG_SPLIT_DELIM_CAPTURE); + + # Create token from text preceding tag. + if ($parts[0] != "") { + $output .= $parts[0]; + } + + # Check if we reach the end. + if (isset($parts[1])) { + $output .= $this->handleSpanToken($parts[1], $parts[2]); + $str = $parts[2]; + } + else { + break; + } + } + + return $output; + } + + + function handleSpanToken($token, &$str) { + # + # Handle $token provided by parseSpan by determining its nature and + # returning the corresponding value that should replace it. + # + switch ($token{0}) { + case "\\": + return $this->hashPart("&#". ord($token{1}). ";"); + case "`": + # Search for end marker in remaining text. + if (preg_match('/^(.*?[^`])'.preg_quote($token).'(?!`)(.*)$/sm', + $str, $matches)) + { + $str = $matches[2]; + $codespan = $this->makeCodeSpan($matches[1]); + return $this->hashPart($codespan); + } + return $token; // return as text since no ending marker found. + default: + return $this->hashPart($token); + } + } + + + function outdent($text) { + # + # Remove one level of line-leading tabs or spaces + # + return preg_replace('/^(\t|[ ]{1,'.$this->tab_width.'})/m', '', $text); + } + + + # String length function for detab. `_initDetab` will create a function to + # hanlde UTF-8 if the default function does not exist. + var $utf8_strlen = 'mb_strlen'; + + function detab($text) { + # + # Replace tabs with the appropriate amount of space. + # + # For each line we separate the line in blocks delemited by + # tab characters. Then we reconstruct every line by adding the + # appropriate number of space between each blocks. + + $text = preg_replace_callback('/^.*\t.*$/m', + array(&$this, '_detab_callback'), $text); + + return $text; + } + function _detab_callback($matches) { + $line = $matches[0]; + $strlen = $this->utf8_strlen; # strlen function for UTF-8. + + # Split in blocks. + $blocks = explode("\t", $line); + # Add each blocks to the line. + $line = $blocks[0]; + unset($blocks[0]); # Do not add first block twice. + foreach ($blocks as $block) { + # Calculate amount of space, insert spaces, insert block. + $amount = $this->tab_width - + $strlen($line, 'UTF-8') % $this->tab_width; + $line .= str_repeat(" ", $amount) . $block; + } + return $line; + } + function _initDetab() { + # + # Check for the availability of the function in the `utf8_strlen` property + # (initially `mb_strlen`). If the function is not available, create a + # function that will loosely count the number of UTF-8 characters with a + # regular expression. + # + if (function_exists($this->utf8_strlen)) return; + $this->utf8_strlen = create_function('$text', 'return preg_match_all( + "/[\\\\x00-\\\\xBF]|[\\\\xC0-\\\\xFF][\\\\x80-\\\\xBF]*/", + $text, $m);'); + } + + + function unhash($text) { + # + # Swap back in all the tags hashed by _HashHTMLBlocks. + # + return preg_replace_callback('/(.)\x1A[0-9]+\1/', + array(&$this, '_unhash_callback'), $text); + } + function _unhash_callback($matches) { + return $this->html_hashes[$matches[0]]; + } + +} + + +# +# Markdown Extra Parser Class +# + +class MarkdownExtra_Parser extends Markdown_Parser { + + # Prefix for footnote ids. + var $fn_id_prefix = ""; + + # Optional title attribute for footnote links and backlinks. + var $fn_link_title = MARKDOWN_FN_LINK_TITLE; + var $fn_backlink_title = MARKDOWN_FN_BACKLINK_TITLE; + + # Optional class attribute for footnote links and backlinks. + var $fn_link_class = MARKDOWN_FN_LINK_CLASS; + var $fn_backlink_class = MARKDOWN_FN_BACKLINK_CLASS; + + # Predefined abbreviations. + var $predef_abbr = array(); + + + function MarkdownExtra_Parser() { + # + # Constructor function. Initialize the parser object. + # + # Add extra escapable characters before parent constructor + # initialize the table. + $this->escape_chars .= ':|'; + + # Insert extra document, block, and span transformations. + # Parent constructor will do the sorting. + $this->document_gamut += array( + "doFencedCodeBlocks" => 5, + "stripFootnotes" => 15, + "stripAbbreviations" => 25, + "appendFootnotes" => 50, + ); + $this->block_gamut += array( + "doFencedCodeBlocks" => 5, + "doTables" => 15, + "doDefLists" => 45, + ); + $this->span_gamut += array( + "doFootnotes" => 5, + "doAbbreviations" => 70, + ); + + parent::Markdown_Parser(); + } + + + # Extra variables used during extra transformations. + var $footnotes = array(); + var $footnotes_ordered = array(); + var $abbr_desciptions = array(); + var $abbr_word_re = ''; + + # Give the current footnote number. + var $footnote_counter = 1; + + + function setup() { + # + # Setting up Extra-specific variables. + # + parent::setup(); + + $this->footnotes = array(); + $this->footnotes_ordered = array(); + $this->abbr_desciptions = array(); + $this->abbr_word_re = ''; + $this->footnote_counter = 1; + + foreach ($this->predef_abbr as $abbr_word => $abbr_desc) { + if ($this->abbr_word_re) + $this->abbr_word_re .= '|'; + $this->abbr_word_re .= preg_quote($abbr_word); + $this->abbr_desciptions[$abbr_word] = trim($abbr_desc); + } + } + + function teardown() { + # + # Clearing Extra-specific variables. + # + $this->footnotes = array(); + $this->footnotes_ordered = array(); + $this->abbr_desciptions = array(); + $this->abbr_word_re = ''; + + parent::teardown(); + } + + + ### HTML Block Parser ### + + # Tags that are always treated as block tags: + var $block_tags_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|form|fieldset|iframe|hr|legend'; + + # Tags treated as block tags only if the opening tag is alone on it's line: + var $context_block_tags_re = 'script|noscript|math|ins|del'; + + # Tags where markdown="1" default to span mode: + var $contain_span_tags_re = 'p|h[1-6]|li|dd|dt|td|th|legend|address'; + + # Tags which must not have their contents modified, no matter where + # they appear: + var $clean_tags_re = 'script|math'; + + # Tags that do not need to be closed. + var $auto_close_tags_re = 'hr|img'; + + + function hashHTMLBlocks($text) { + # + # Hashify HTML Blocks and "clean tags". + # + # We only want to do this for block-level HTML tags, such as headers, + # lists, and tables. That's because we still want to wrap

    s around + # "paragraphs" that are wrapped in non-block-level tags, such as anchors, + # phrase emphasis, and spans. The list of tags we're looking for is + # hard-coded. + # + # This works by calling _HashHTMLBlocks_InMarkdown, which then calls + # _HashHTMLBlocks_InHTML when it encounter block tags. When the markdown="1" + # attribute is found whitin a tag, _HashHTMLBlocks_InHTML calls back + # _HashHTMLBlocks_InMarkdown to handle the Markdown syntax within the tag. + # These two functions are calling each other. It's recursive! + # + # + # Call the HTML-in-Markdown hasher. + # + list($text, ) = $this->_hashHTMLBlocks_inMarkdown($text); + + return $text; + } + function _hashHTMLBlocks_inMarkdown($text, $indent = 0, + $enclosing_tag_re = '', $span = false) + { + # + # Parse markdown text, calling _HashHTMLBlocks_InHTML for block tags. + # + # * $indent is the number of space to be ignored when checking for code + # blocks. This is important because if we don't take the indent into + # account, something like this (which looks right) won't work as expected: + # + #

    + #
    + # Hello World. <-- Is this a Markdown code block or text? + #
    <-- Is this a Markdown code block or a real tag? + #
    + # + # If you don't like this, just don't indent the tag on which + # you apply the markdown="1" attribute. + # + # * If $enclosing_tag_re is not empty, stops at the first unmatched closing + # tag with that name. Nested tags supported. + # + # * If $span is true, text inside must treated as span. So any double + # newline will be replaced by a single newline so that it does not create + # paragraphs. + # + # Returns an array of that form: ( processed text , remaining text ) + # + if ($text === '') return array('', ''); + + # Regex to check for the presense of newlines around a block tag. + $newline_before_re = '/(?:^\n?|\n\n)*$/'; + $newline_after_re = + '{ + ^ # Start of text following the tag. + (?>[ ]*)? # Optional comment. + [ ]*\n # Must be followed by newline. + }xs'; + + # Regex to match any tag. + $block_tag_re = + '{ + ( # $2: Capture hole tag. + # Tag name. + '.$this->block_tags_re.' | + '.$this->context_block_tags_re.' | + '.$this->clean_tags_re.' | + (?!\s)'.$enclosing_tag_re.' + ) + (?: + (?=[\s"\'/a-zA-Z0-9]) # Allowed characters after tag name. + (?> + ".*?" | # Double quotes (can contain `>`) + \'.*?\' | # Single quotes (can contain `>`) + .+? # Anything but quotes and `>`. + )*? + )? + > # End of tag. + | + # HTML Comment + | + <\?.*?\?> | <%.*?%> # Processing instruction + | + # CData Block + | + # Code span marker + `+ + '. ( !$span ? ' # If not in span. + | + # Indented code block + (?: ^[ ]*\n | ^ | \n[ ]*\n ) + [ ]{'.($indent+4).'}[^\n]* \n + (?> + (?: [ ]{'.($indent+4).'}[^\n]* | [ ]* ) \n + )* + | + # Fenced code block marker + (?> ^ | \n ) + [ ]{0,'.($indent).'}~~~+[ ]*\n + ' : '' ). ' # End (if not is span). + ) + }xs'; + + + $depth = 0; # Current depth inside the tag tree. + $parsed = ""; # Parsed text that will be returned. + + # + # Loop through every tag until we find the closing tag of the parent + # or loop until reaching the end of text if no parent tag specified. + # + do { + # + # Split the text using the first $tag_match pattern found. + # Text before pattern will be first in the array, text after + # pattern will be at the end, and between will be any catches made + # by the pattern. + # + $parts = preg_split($block_tag_re, $text, 2, + PREG_SPLIT_DELIM_CAPTURE); + + # If in Markdown span mode, add a empty-string span-level hash + # after each newline to prevent triggering any block element. + if ($span) { + $void = $this->hashPart("", ':'); + $newline = "$void\n"; + $parts[0] = $void . str_replace("\n", $newline, $parts[0]) . $void; + } + + $parsed .= $parts[0]; # Text before current tag. + + # If end of $text has been reached. Stop loop. + if (count($parts) < 3) { + $text = ""; + break; + } + + $tag = $parts[1]; # Tag to handle. + $text = $parts[2]; # Remaining text after current tag. + $tag_re = preg_quote($tag); # For use in a regular expression. + + # + # Check for: Code span marker + # + if ($tag{0} == "`") { + # Find corresponding end marker. + $tag_re = preg_quote($tag); + if (preg_match('{^(?>.+?|\n(?!\n))*?(?.*\n)+?[ ]{0,'.($indent).'}'.$tag_re.'[ ]*\n}', $text, + $matches)) + { + # End marker found: pass text unchanged until marker. + $parsed .= $tag . $matches[0]; + $text = substr($text, strlen($matches[0])); + } + else { + # No end marker: just skip it. + $parsed .= $tag; + } + } + # + # Check for: Indented code block. + # + else if ($tag{0} == "\n" || $tag{0} == " ") { + # Indented code block: pass it unchanged, will be handled + # later. + $parsed .= $tag; + } + # + # Check for: Opening Block level tag or + # Opening Context Block tag (like ins and del) + # used as a block tag (tag is alone on it's line). + # + else if (preg_match('{^<(?:'.$this->block_tags_re.')\b}', $tag) || + ( preg_match('{^<(?:'.$this->context_block_tags_re.')\b}', $tag) && + preg_match($newline_before_re, $parsed) && + preg_match($newline_after_re, $text) ) + ) + { + # Need to parse tag and following text using the HTML parser. + list($block_text, $text) = + $this->_hashHTMLBlocks_inHTML($tag . $text, "hashBlock", true); + + # Make sure it stays outside of any paragraph by adding newlines. + $parsed .= "\n\n$block_text\n\n"; + } + # + # Check for: Clean tag (like script, math) + # HTML Comments, processing instructions. + # + else if (preg_match('{^<(?:'.$this->clean_tags_re.')\b}', $tag) || + $tag{1} == '!' || $tag{1} == '?') + { + # Need to parse tag and following text using the HTML parser. + # (don't check for markdown attribute) + list($block_text, $text) = + $this->_hashHTMLBlocks_inHTML($tag . $text, "hashClean", false); + + $parsed .= $block_text; + } + # + # Check for: Tag with same name as enclosing tag. + # + else if ($enclosing_tag_re !== '' && + # Same name as enclosing tag. + preg_match('{^= 0); + + return array($parsed, $text); + } + function _hashHTMLBlocks_inHTML($text, $hash_method, $md_attr) { + # + # Parse HTML, calling _HashHTMLBlocks_InMarkdown for block tags. + # + # * Calls $hash_method to convert any blocks. + # * Stops when the first opening tag closes. + # * $md_attr indicate if the use of the `markdown="1"` attribute is allowed. + # (it is not inside clean tags) + # + # Returns an array of that form: ( processed text , remaining text ) + # + if ($text === '') return array('', ''); + + # Regex to match `markdown` attribute inside of a tag. + $markdown_attr_re = ' + { + \s* # Eat whitespace before the `markdown` attribute + markdown + \s*=\s* + (?> + (["\']) # $1: quote delimiter + (.*?) # $2: attribute value + \1 # matching delimiter + | + ([^\s>]*) # $3: unquoted attribute value + ) + () # $4: make $3 always defined (avoid warnings) + }xs'; + + # Regex to match any tag. + $tag_re = '{ + ( # $2: Capture hole tag. + + ".*?" | # Double quotes (can contain `>`) + \'.*?\' | # Single quotes (can contain `>`) + .+? # Anything but quotes and `>`. + )*? + )? + > # End of tag. + | + # HTML Comment + | + <\?.*?\?> | <%.*?%> # Processing instruction + | + # CData Block + ) + }xs'; + + $original_text = $text; # Save original text in case of faliure. + + $depth = 0; # Current depth inside the tag tree. + $block_text = ""; # Temporary text holder for current text. + $parsed = ""; # Parsed text that will be returned. + + # + # Get the name of the starting tag. + # (This pattern makes $base_tag_name_re safe without quoting.) + # + if (preg_match('/^<([\w:$]*)\b/', $text, $matches)) + $base_tag_name_re = $matches[1]; + + # + # Loop through every tag until we find the corresponding closing tag. + # + do { + # + # Split the text using the first $tag_match pattern found. + # Text before pattern will be first in the array, text after + # pattern will be at the end, and between will be any catches made + # by the pattern. + # + $parts = preg_split($tag_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE); + + if (count($parts) < 3) { + # + # End of $text reached with unbalenced tag(s). + # In that case, we return original text unchanged and pass the + # first character as filtered to prevent an infinite loop in the + # parent function. + # + return array($original_text{0}, substr($original_text, 1)); + } + + $block_text .= $parts[0]; # Text before current tag. + $tag = $parts[1]; # Tag to handle. + $text = $parts[2]; # Remaining text after current tag. + + # + # Check for: Auto-close tag (like
    ) + # Comments and Processing Instructions. + # + if (preg_match('{^auto_close_tags_re.')\b}', $tag) || + $tag{1} == '!' || $tag{1} == '?') + { + # Just add the tag to the block as if it was text. + $block_text .= $tag; + } + else { + # + # Increase/decrease nested tag count. Only do so if + # the tag's name match base tag's. + # + if (preg_match('{^mode = $attr_m[2] . $attr_m[3]; + $span_mode = $this->mode == 'span' || $this->mode != 'block' && + preg_match('{^<(?:'.$this->contain_span_tags_re.')\b}', $tag); + + # Calculate indent before tag. + if (preg_match('/(?:^|\n)( *?)(?! ).*?$/', $block_text, $matches)) { + $strlen = $this->utf8_strlen; + $indent = $strlen($matches[1], 'UTF-8'); + } else { + $indent = 0; + } + + # End preceding block with this tag. + $block_text .= $tag; + $parsed .= $this->$hash_method($block_text); + + # Get enclosing tag name for the ParseMarkdown function. + # (This pattern makes $tag_name_re safe without quoting.) + preg_match('/^<([\w:$]*)\b/', $tag, $matches); + $tag_name_re = $matches[1]; + + # Parse the content using the HTML-in-Markdown parser. + list ($block_text, $text) + = $this->_hashHTMLBlocks_inMarkdown($text, $indent, + $tag_name_re, $span_mode); + + # Outdent markdown text. + if ($indent > 0) { + $block_text = preg_replace("/^[ ]{1,$indent}/m", "", + $block_text); + } + + # Append tag content to parsed text. + if (!$span_mode) $parsed .= "\n\n$block_text\n\n"; + else $parsed .= "$block_text"; + + # Start over a new block. + $block_text = ""; + } + else $block_text .= $tag; + } + + } while ($depth > 0); + + # + # Hash last block text that wasn't processed inside the loop. + # + $parsed .= $this->$hash_method($block_text); + + return array($parsed, $text); + } + + + function hashClean($text) { + # + # Called whenever a tag must be hashed when a function insert a "clean" tag + # in $text, it pass through this function and is automaticaly escaped, + # blocking invalid nested overlap. + # + return $this->hashPart($text, 'C'); + } + + + function doHeaders($text) { + # + # Redefined to add id attribute support. + # + # Setext-style headers: + # Header 1 {#header1} + # ======== + # + # Header 2 {#header2} + # -------- + # + $text = preg_replace_callback( + '{ + (^.+?) # $1: Header text + (?:[ ]+\{\#([-_:a-zA-Z0-9]+)\})? # $2: Id attribute + [ ]*\n(=+|-+)[ ]*\n+ # $3: Header footer + }mx', + array(&$this, '_doHeaders_callback_setext'), $text); + + # atx-style headers: + # # Header 1 {#header1} + # ## Header 2 {#header2} + # ## Header 2 with closing hashes ## {#header3} + # ... + # ###### Header 6 {#header2} + # + $text = preg_replace_callback('{ + ^(\#{1,6}) # $1 = string of #\'s + [ ]* + (.+?) # $2 = Header text + [ ]* + \#* # optional closing #\'s (not counted) + (?:[ ]+\{\#([-_:a-zA-Z0-9]+)\})? # id attribute + [ ]* + \n+ + }xm', + array(&$this, '_doHeaders_callback_atx'), $text); + + return $text; + } + function _doHeaders_attr($attr) { + if (empty($attr)) return ""; + return " id=\"$attr\""; + } + function _doHeaders_callback_setext($matches) { + if ($matches[3] == '-' && preg_match('{^- }', $matches[1])) + return $matches[0]; + $level = $matches[3]{0} == '=' ? 1 : 2; + $attr = $this->_doHeaders_attr($id =& $matches[2]); + $block = "".$this->runSpanGamut($matches[1]).""; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + function _doHeaders_callback_atx($matches) { + $level = strlen($matches[1]); + $attr = $this->_doHeaders_attr($id =& $matches[3]); + $block = "".$this->runSpanGamut($matches[2]).""; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + + + function doTables($text) { + # + # Form HTML tables. + # + $less_than_tab = $this->tab_width - 1; + # + # Find tables with leading pipe. + # + # | Header 1 | Header 2 + # | -------- | -------- + # | Cell 1 | Cell 2 + # | Cell 3 | Cell 4 + # + $text = preg_replace_callback(' + { + ^ # Start of a line + [ ]{0,'.$less_than_tab.'} # Allowed whitespace. + [|] # Optional leading pipe (present) + (.+) \n # $1: Header row (at least one pipe) + + [ ]{0,'.$less_than_tab.'} # Allowed whitespace. + [|] ([ ]*[-:]+[-| :]*) \n # $2: Header underline + + ( # $3: Cells + (?> + [ ]* # Allowed whitespace. + [|] .* \n # Row content. + )* + ) + (?=\n|\Z) # Stop at final double newline. + }xm', + array(&$this, '_doTable_leadingPipe_callback'), $text); + + # + # Find tables without leading pipe. + # + # Header 1 | Header 2 + # -------- | -------- + # Cell 1 | Cell 2 + # Cell 3 | Cell 4 + # + $text = preg_replace_callback(' + { + ^ # Start of a line + [ ]{0,'.$less_than_tab.'} # Allowed whitespace. + (\S.*[|].*) \n # $1: Header row (at least one pipe) + + [ ]{0,'.$less_than_tab.'} # Allowed whitespace. + ([-:]+[ ]*[|][-| :]*) \n # $2: Header underline + + ( # $3: Cells + (?> + .* [|] .* \n # Row content + )* + ) + (?=\n|\Z) # Stop at final double newline. + }xm', + array(&$this, '_DoTable_callback'), $text); + + return $text; + } + function _doTable_leadingPipe_callback($matches) { + $head = $matches[1]; + $underline = $matches[2]; + $content = $matches[3]; + + # Remove leading pipe for each row. + $content = preg_replace('/^ *[|]/m', '', $content); + + return $this->_doTable_callback(array($matches[0], $head, $underline, $content)); + } + function _doTable_callback($matches) { + $head = $matches[1]; + $underline = $matches[2]; + $content = $matches[3]; + + # Remove any tailing pipes for each line. + $head = preg_replace('/[|] *$/m', '', $head); + $underline = preg_replace('/[|] *$/m', '', $underline); + $content = preg_replace('/[|] *$/m', '', $content); + + # Reading alignement from header underline. + $separators = preg_split('/ *[|] */', $underline); + foreach ($separators as $n => $s) { + if (preg_match('/^ *-+: *$/', $s)) $attr[$n] = ' align="right"'; + else if (preg_match('/^ *:-+: *$/', $s))$attr[$n] = ' align="center"'; + else if (preg_match('/^ *:-+ *$/', $s)) $attr[$n] = ' align="left"'; + else $attr[$n] = ''; + } + + # Parsing span elements, including code spans, character escapes, + # and inline HTML tags, so that pipes inside those gets ignored. + $head = $this->parseSpan($head); + $headers = preg_split('/ *[|] */', $head); + $col_count = count($headers); + + # Write column headers. + $text = "\n"; + $text .= "\n"; + $text .= "\n"; + foreach ($headers as $n => $header) + $text .= " ".$this->runSpanGamut(trim($header))."\n"; + $text .= "\n"; + $text .= "\n"; + + # Split content by row. + $rows = explode("\n", trim($content, "\n")); + + $text .= "\n"; + foreach ($rows as $row) { + # Parsing span elements, including code spans, character escapes, + # and inline HTML tags, so that pipes inside those gets ignored. + $row = $this->parseSpan($row); + + # Split row by cell. + $row_cells = preg_split('/ *[|] */', $row, $col_count); + $row_cells = array_pad($row_cells, $col_count, ''); + + $text .= "\n"; + foreach ($row_cells as $n => $cell) + $text .= " ".$this->runSpanGamut(trim($cell))."\n"; + $text .= "\n"; + } + $text .= "\n"; + $text .= "
    "; + + return $this->hashBlock($text) . "\n"; + } + + + function doDefLists($text) { + # + # Form HTML definition lists. + # + $less_than_tab = $this->tab_width - 1; + + # Re-usable pattern to match any entire dl list: + $whole_list_re = '(?> + ( # $1 = whole list + ( # $2 + [ ]{0,'.$less_than_tab.'} + ((?>.*\S.*\n)+) # $3 = defined term + \n? + [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition + ) + (?s:.+?) + ( # $4 + \z + | + \n{2,} + (?=\S) + (?! # Negative lookahead for another term + [ ]{0,'.$less_than_tab.'} + (?: \S.*\n )+? # defined term + \n? + [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition + ) + (?! # Negative lookahead for another definition + [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition + ) + ) + ) + )'; // mx + + $text = preg_replace_callback('{ + (?>\A\n?|(?<=\n\n)) + '.$whole_list_re.' + }mx', + array(&$this, '_doDefLists_callback'), $text); + + return $text; + } + function _doDefLists_callback($matches) { + # Re-usable patterns to match list item bullets and number markers: + $list = $matches[1]; + + # Turn double returns into triple returns, so that we can make a + # paragraph for the last item in a list, if necessary: + $result = trim($this->processDefListItems($list)); + $result = "
    \n" . $result . "\n
    "; + return $this->hashBlock($result) . "\n\n"; + } + + + function processDefListItems($list_str) { + # + # Process the contents of a single definition list, splitting it + # into individual term and definition list items. + # + $less_than_tab = $this->tab_width - 1; + + # trim trailing blank lines: + $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str); + + # Process definition terms. + $list_str = preg_replace_callback('{ + (?>\A\n?|\n\n+) # leading line + ( # definition terms = $1 + [ ]{0,'.$less_than_tab.'} # leading whitespace + (?![:][ ]|[ ]) # negative lookahead for a definition + # mark (colon) or more whitespace. + (?> \S.* \n)+? # actual term (not whitespace). + ) + (?=\n?[ ]{0,3}:[ ]) # lookahead for following line feed + # with a definition mark. + }xm', + array(&$this, '_processDefListItems_callback_dt'), $list_str); + + # Process actual definitions. + $list_str = preg_replace_callback('{ + \n(\n+)? # leading line = $1 + ( # marker space = $2 + [ ]{0,'.$less_than_tab.'} # whitespace before colon + [:][ ]+ # definition mark (colon) + ) + ((?s:.+?)) # definition text = $3 + (?= \n+ # stop at next definition mark, + (?: # next term or end of text + [ ]{0,'.$less_than_tab.'} [:][ ] | +
    | \z + ) + ) + }xm', + array(&$this, '_processDefListItems_callback_dd'), $list_str); + + return $list_str; + } + function _processDefListItems_callback_dt($matches) { + $terms = explode("\n", trim($matches[1])); + $text = ''; + foreach ($terms as $term) { + $term = $this->runSpanGamut(trim($term)); + $text .= "\n
    " . $term . "
    "; + } + return $text . "\n"; + } + function _processDefListItems_callback_dd($matches) { + $leading_line = $matches[1]; + $marker_space = $matches[2]; + $def = $matches[3]; + + if ($leading_line || preg_match('/\n{2,}/', $def)) { + # Replace marker with the appropriate whitespace indentation + $def = str_repeat(' ', strlen($marker_space)) . $def; + $def = $this->runBlockGamut($this->outdent($def . "\n\n")); + $def = "\n". $def ."\n"; + } + else { + $def = rtrim($def); + $def = $this->runSpanGamut($this->outdent($def)); + } + + return "\n
    " . $def . "
    \n"; + } + + + function doFencedCodeBlocks($text) { + # + # Adding the fenced code block syntax to regular Markdown: + # + # ~~~ + # Code block + # ~~~ + # + $less_than_tab = $this->tab_width; + + $text = preg_replace_callback('{ + (?:\n|\A) + # 1: Opening marker + ( + ~{3,} # Marker: three tilde or more. + ) + [ ]* \n # Whitespace and newline following marker. + + # 2: Content + ( + (?> + (?!\1 [ ]* \n) # Not a closing marker. + .*\n+ + )+ + ) + + # Closing marker. + \1 [ ]* \n + }xm', + array(&$this, '_doFencedCodeBlocks_callback'), $text); + + return $text; + } + function _doFencedCodeBlocks_callback($matches) { + $codeblock = $matches[2]; + $codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES); + $codeblock = preg_replace_callback('/^\n+/', + array(&$this, '_doFencedCodeBlocks_newlines'), $codeblock); + $codeblock = "
    $codeblock
    "; + return "\n\n".$this->hashBlock($codeblock)."\n\n"; + } + function _doFencedCodeBlocks_newlines($matches) { + return str_repeat("empty_element_suffix", + strlen($matches[0])); + } + + + # + # Redefining emphasis markers so that emphasis by underscore does not + # work in the middle of a word. + # + var $em_relist = array( + '' => '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? tags + # + # Strip leading and trailing lines: + $text = preg_replace('/\A\n+|\n+\z/', '', $text); + + $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY); + + # + # Wrap

    tags and unhashify HTML blocks + # + foreach ($grafs as $key => $value) { + $value = trim($this->runSpanGamut($value)); + + # Check if this should be enclosed in a paragraph. + # Clean tag hashes & block tag hashes are left alone. + $is_p = !preg_match('/^B\x1A[0-9]+B|^C\x1A[0-9]+C$/', $value); + + if ($is_p) { + $value = "

    $value

    "; + } + $grafs[$key] = $value; + } + + # Join grafs in one text, then unhash HTML tags. + $text = implode("\n\n", $grafs); + + # Finish by removing any tag hashes still present in $text. + $text = $this->unhash($text); + + return $text; + } + + + ### Footnotes + + function stripFootnotes($text) { + # + # Strips link definitions from text, stores the URLs and titles in + # hash references. + # + $less_than_tab = $this->tab_width - 1; + + # Link defs are in the form: [^id]: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,'.$less_than_tab.'}\[\^(.+?)\][ ]?: # note_id = $1 + [ ]* + \n? # maybe *one* newline + ( # text = $2 (no blank lines allowed) + (?: + .+ # actual text + | + \n # newlines but + (?!\[\^.+?\]:\s)# negative lookahead for footnote marker. + (?!\n+[ ]{0,3}\S)# ensure line is not blank and followed + # by non-indented content + )* + ) + }xm', + array(&$this, '_stripFootnotes_callback'), + $text); + return $text; + } + function _stripFootnotes_callback($matches) { + $note_id = $this->fn_id_prefix . $matches[1]; + $this->footnotes[$note_id] = $this->outdent($matches[2]); + return ''; # String that will replace the block + } + + + function doFootnotes($text) { + # + # Replace footnote references in $text [^id] with a special text-token + # which will be replaced by the actual footnote marker in appendFootnotes. + # + if (!$this->in_anchor) { + $text = preg_replace('{\[\^(.+?)\]}', "F\x1Afn:\\1\x1A:", $text); + } + return $text; + } + + + function appendFootnotes($text) { + # + # Append footnote list to text. + # + $text = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}', + array(&$this, '_appendFootnotes_callback'), $text); + + if (!empty($this->footnotes_ordered)) { + $text .= "\n\n"; + $text .= "
    \n"; + $text .= "empty_element_suffix ."\n"; + $text .= "
      \n\n"; + + $attr = " rev=\"footnote\""; + if ($this->fn_backlink_class != "") { + $class = $this->fn_backlink_class; + $class = $this->encodeAttribute($class); + $attr .= " class=\"$class\""; + } + if ($this->fn_backlink_title != "") { + $title = $this->fn_backlink_title; + $title = $this->encodeAttribute($title); + $attr .= " title=\"$title\""; + } + $num = 0; + + while (!empty($this->footnotes_ordered)) { + $footnote = reset($this->footnotes_ordered); + $note_id = key($this->footnotes_ordered); + unset($this->footnotes_ordered[$note_id]); + + $footnote .= "\n"; # Need to append newline before parsing. + $footnote = $this->runBlockGamut("$footnote\n"); + $footnote = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}', + array(&$this, '_appendFootnotes_callback'), $footnote); + + $attr = str_replace("%%", ++$num, $attr); + $note_id = $this->encodeAttribute($note_id); + + # Add backlink to last paragraph; create new paragraph if needed. + $backlink = ""; + if (preg_match('{

      $}', $footnote)) { + $footnote = substr($footnote, 0, -4) . " $backlink

      "; + } else { + $footnote .= "\n\n

      $backlink

      "; + } + + $text .= "
    1. \n"; + $text .= $footnote . "\n"; + $text .= "
    2. \n\n"; + } + + $text .= "
    \n"; + $text .= "
    "; + } + return $text; + } + function _appendFootnotes_callback($matches) { + $node_id = $this->fn_id_prefix . $matches[1]; + + # Create footnote marker only if it has a corresponding footnote *and* + # the footnote hasn't been used by another marker. + if (isset($this->footnotes[$node_id])) { + # Transfert footnote content to the ordered list. + $this->footnotes_ordered[$node_id] = $this->footnotes[$node_id]; + unset($this->footnotes[$node_id]); + + $num = $this->footnote_counter++; + $attr = " rel=\"footnote\""; + if ($this->fn_link_class != "") { + $class = $this->fn_link_class; + $class = $this->encodeAttribute($class); + $attr .= " class=\"$class\""; + } + if ($this->fn_link_title != "") { + $title = $this->fn_link_title; + $title = $this->encodeAttribute($title); + $attr .= " title=\"$title\""; + } + + $attr = str_replace("%%", $num, $attr); + $node_id = $this->encodeAttribute($node_id); + + return + "". + "$num". + ""; + } + + return "[^".$matches[1]."]"; + } + + + ### Abbreviations ### + + function stripAbbreviations($text) { + # + # Strips abbreviations from text, stores titles in hash references. + # + $less_than_tab = $this->tab_width - 1; + + # Link defs are in the form: [id]*: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,'.$less_than_tab.'}\*\[(.+?)\][ ]?: # abbr_id = $1 + (.*) # text = $2 (no blank lines allowed) + }xm', + array(&$this, '_stripAbbreviations_callback'), + $text); + return $text; + } + function _stripAbbreviations_callback($matches) { + $abbr_word = $matches[1]; + $abbr_desc = $matches[2]; + if ($this->abbr_word_re) + $this->abbr_word_re .= '|'; + $this->abbr_word_re .= preg_quote($abbr_word); + $this->abbr_desciptions[$abbr_word] = trim($abbr_desc); + return ''; # String that will replace the block + } + + + function doAbbreviations($text) { + # + # Find defined abbreviations in text and wrap them in elements. + # + if ($this->abbr_word_re) { + // cannot use the /x modifier because abbr_word_re may + // contain significant spaces: + $text = preg_replace_callback('{'. + '(?abbr_word_re.')'. + '(?![\w\x1A])'. + '}', + array(&$this, '_doAbbreviations_callback'), $text); + } + return $text; + } + function _doAbbreviations_callback($matches) { + $abbr = $matches[0]; + if (isset($this->abbr_desciptions[$abbr])) { + $desc = $this->abbr_desciptions[$abbr]; + if (empty($desc)) { + return $this->hashPart("$abbr"); + } else { + $desc = $this->encodeAttribute($desc); + return $this->hashPart("$abbr"); + } + } else { + return $matches[0]; + } + } + +} + + +/* + +PHP Markdown Extra +================== + +Description +----------- + +This is a PHP port of the original Markdown formatter written in Perl +by John Gruber. This special "Extra" version of PHP Markdown features +further enhancements to the syntax for making additional constructs +such as tables and definition list. + +Markdown is a text-to-HTML filter; it translates an easy-to-read / +easy-to-write structured text format into HTML. Markdown's text format +is most similar to that of plain text email, and supports features such +as headers, *emphasis*, code blocks, blockquotes, and links. + +Markdown's syntax is designed not as a generic markup language, but +specifically to serve as a front-end to (X)HTML. You can use span-level +HTML tags anywhere in a Markdown document, and you can use block level +HTML tags (like
    and as well). + +For more information about Markdown's syntax, see: + + + + +Bugs +---- + +To file bug reports please send email to: + + + +Please include with your report: (1) the example input; (2) the output you +expected; (3) the output Markdown actually produced. + + +Version History +--------------- + +See the readme file for detailed release notes for this version. + + +Copyright and License +--------------------- + +PHP Markdown & Extra +Copyright (c) 2004-2009 Michel Fortin + +All rights reserved. + +Based on Markdown +Copyright (c) 2003-2006 John Gruber + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +* Neither the name "Markdown" nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +This software is provided by the copyright holders and contributors "as +is" and any express or implied warranties, including, but not limited +to, the implied warranties of merchantability and fitness for a +particular purpose are disclaimed. In no event shall the copyright owner +or contributors be liable for any direct, indirect, incidental, special, +exemplary, or consequential damages (including, but not limited to, +procurement of substitute goods or services; loss of use, data, or +profits; or business interruption) however caused and on any theory of +liability, whether in contract, strict liability, or tort (including +negligence or otherwise) arising in any way out of the use of this +software, even if advised of the possibility of such damage. + +*/ +?> \ No newline at end of file diff --git a/docs/lib/markdown/readme.md b/docs/lib/markdown/readme.md new file mode 100755 index 0000000..e698576 --- /dev/null +++ b/docs/lib/markdown/readme.md @@ -0,0 +1,802 @@ +PHP Markdown Extra +================== + +Version 1.2.5 - Sun 8 Jan 2012 + +by Michel Fortin + + +based on Markdown by John Gruber + + + +Introduction +------------ + +This is a special version of PHP Markdown with extra features. See + for details. + +Markdown is a text-to-HTML conversion tool for web writers. Markdown +allows you to write using an easy-to-read, easy-to-write plain text +format, then convert it to structurally valid XHTML (or HTML). + +"Markdown" is two things: a plain text markup syntax, and a software +tool, written in Perl, that converts the plain text markup to HTML. +PHP Markdown is a port to PHP of the original Markdown program by +John Gruber. + +PHP Markdown can work as a plug-in for WordPress and bBlog, as a +modifier for the Smarty templating engine, or as a remplacement for +textile formatting in any software that support textile. + +Full documentation of Markdown's syntax is available on John's +Markdown page: + + +Installation and Requirement +---------------------------- + +PHP Markdown requires PHP version 4.0.5 or later. + + +### WordPress ### + +PHP Markdown works with [WordPress][wp], version 1.2 or later. + + [wp]: http://wordpress.org/ + +1. To use PHP Markdown with WordPress, place the "makrdown.php" file + in the "plugins" folder. This folder is located inside + "wp-content" at the root of your site: + + (site home)/wp-content/plugins/ + +2. Activate the plugin with the administrative interface of + WordPress. In the "Plugins" section you will now find Markdown. + To activate the plugin, click on the "Activate" button on the + same line than Markdown. Your entries will now be formatted by + PHP Markdown. + +3. To post Markdown content, you'll first have to disable the + "visual" editor in the User section of WordPress. + +You can configure PHP Markdown to not apply to the comments on your +WordPress weblog. See the "Configuration" section below. + +It is not possible at this time to apply a different set of +filters to different entries. All your entries will be formated by +PHP Markdown. This is a limitation of WordPress. If your old entries +are written in HTML (as opposed to another formatting syntax, like +Textile), they'll probably stay fine after installing Markdown. + + +### bBlog ### + +PHP Markdown also works with [bBlog][bb]. + + [bb]: http://www.bblog.com/ + +To use PHP Markdown with bBlog, rename "markdown.php" to +"modifier.markdown.php" and place the file in the "bBlog_plugins" +folder. This folder is located inside the "bblog" directory of +your site, like this: + + (site home)/bblog/bBlog_plugins/modifier.markdown.php + +Select "Markdown" as the "Entry Modifier" when you post a new +entry. This setting will only apply to the entry you are editing. + + +### Replacing Textile in TextPattern ### + +[TextPattern][tp] use [Textile][tx] to format your text. You can +replace Textile by Markdown in TextPattern without having to change +any code by using the *Texitle Compatibility Mode*. This may work +with other software that expect Textile too. + + [tx]: http://www.textism.com/tools/textile/ + [tp]: http://www.textpattern.com/ + +1. Rename the "markdown.php" file to "classTextile.php". This will + make PHP Markdown behave as if it was the actual Textile parser. + +2. Replace the "classTextile.php" file TextPattern installed in your + web directory. It can be found in the "lib" directory: + + (site home)/textpattern/lib/ + +Contrary to Textile, Markdown does not convert quotes to curly ones +and does not convert multiple hyphens (`--` and `---`) into en- and +em-dashes. If you use PHP Markdown in Textile Compatibility Mode, you +can solve this problem by installing the "smartypants.php" file from +[PHP SmartyPants][psp] beside the "classTextile.php" file. The Textile +Compatibility Mode function will use SmartyPants automatically without +further modification. + + [psp]: http://michelf.com/projects/php-smartypants/ + + +### In Your Own Programs ### + +You can use PHP Markdown easily in your current PHP program. Simply +include the file and then call the Markdown function on the text you +want to convert: + + include_once "markdown.php"; + $my_html = Markdown($my_text); + +If you wish to use PHP Markdown with another text filter function +built to parse HTML, you should filter the text *after* the Markdown +function call. This is an example with [PHP SmartyPants][psp]: + + $my_html = SmartyPants(Markdown($my_text)); + + +### With Smarty ### + +If your program use the [Smarty][sm] template engine, PHP Markdown +can now be used as a modifier for your templates. Rename "markdown.php" +to "modifier.markdown.php" and put it in your smarty plugins folder. + + [sm]: http://smarty.php.net/ + +If you are using MovableType 3.1 or later, the Smarty plugin folder is +located at `(MT CGI root)/php/extlib/smarty/plugins`. This will allow +Markdown to work on dynamic pages. + + +### Updating Markdown in Other Programs ### + +Many web applications now ship with PHP Markdown, or have plugins to +perform the conversion to HTML. You can update PHP Markdown -- or +replace it with PHP Markdown Extra -- in many of these programs by +swapping the old "markdown.php" file for the new one. + +Here is a short non-exhaustive list of some programs and where they +hide the "markdown.php" file. + +| Program | Path to Markdown +| ------- | ---------------- +| [Pivot][] | `(site home)/pivot/includes/markdown/` + +If you're unsure if you can do this with your application, ask the +developer, or wait for the developer to update his application or +plugin with the new version of PHP Markdown. + + [Pivot]: http://pivotlog.net/ + + +Configuration +------------- + +By default, PHP Markdown produces XHTML output for tags with empty +elements. E.g.: + +
    + +Markdown can be configured to produce HTML-style tags; e.g.: + +
    + +To do this, you must edit the "MARKDOWN_EMPTY_ELEMENT_SUFFIX" +definition below the "Global default settings" header at the start of +the "markdown.php" file. + + +### WordPress-Specific Settings ### + +By default, the Markdown plugin applies to both posts and comments on +your WordPress weblog. To deactivate one or the other, edit the +`MARKDOWN_WP_POSTS` or `MARKDOWN_WP_COMMENTS` definitions under the +"WordPress settings" header at the start of the "markdown.php" file. + + +Bugs +---- + +To file bug reports please send email to: + + +Please include with your report: (1) the example input; (2) the output you +expected; (3) the output PHP Markdown actually produced. + + +Version History +--------------- + +1.0.1o (8 Jan 2012): + +* Silenced a new warning introduced around PHP 5.3 complaining about + POSIX characters classes not being implemented. PHP Markdown does not + use POSIX character classes, but it nevertheless trigged that warning. + + +Extra 1.2.5 (8 Jan 2012): + +* Fixed an issue preventing fenced code blocks indented inside lists items + and elsewhere from being interpreted correctly. + +* Fixed an issue where HTML tags inside fenced code blocks were sometime + not encoded with entities. + + +1.0.1n (10 Oct 2009): + +* Enabled reference-style shortcut links. Now you can write reference-style + links with less brakets: + + This is [my website]. + + [my website]: http://example.com/ + + This was added in the 1.0.2 betas, but commented out in the 1.0.1 branch, + waiting for the feature to be officialized. [But half of the other Markdown + implementations are supporting this syntax][half], so it makes sense for + compatibility's sake to allow it in PHP Markdown too. + + [half]: http://babelmark.bobtfish.net/?markdown=This+is+%5Bmy+website%5D.%0D%0A%09%09%0D%0A%5Bmy+website%5D%3A+http%3A%2F%2Fexample.com%2F%0D%0A&src=1&dest=2 + +* Now accepting many valid email addresses in autolinks that were + previously rejected, such as: + + + + <"abc@def"@example.com> + <"Fred Bloggs"@example.com> + + +* Now accepting spaces in URLs for inline and reference-style links. Such + URLs need to be surrounded by angle brakets. For instance: + + [link text]( "optional title") + + [link text][ref] + [ref]: "optional title" + + There is still a quirk which may prevent this from working correctly with + relative URLs in inline-style links however. + +* Fix for adjacent list of different kind where the second list could + end as a sublist of the first when not separated by an empty line. + +* Fixed a bug where inline-style links wouldn't be recognized when the link + definition contains a line break between the url and the title. + +* Fixed a bug where tags where the name contains an underscore aren't parsed + correctly. + +* Fixed some corner-cases mixing underscore-ephasis and asterisk-emphasis. + + +Extra 1.2.4 (10 Oct 2009): + +* Fixed a problem where unterminated tags in indented code blocks could + prevent proper escaping of characaters in the code block. + + +Extra 1.2.3 (31 Dec 2008): + +* In WordPress pages featuring more than one post, footnote id prefixes are + now automatically applied with the current post ID to avoid clashes + between footnotes belonging to different posts. + +* Fix for a bug introduced in Extra 1.2 where block-level HTML tags where + not detected correctly, thus the addition of erroneous `

    ` tags and + interpretation of their content as Markdown-formatted instead of + HTML-formatted. + + +Extra 1.2.2 (21 Jun 2008): + +* Fixed a problem where abbreviation definitions, footnote + definitions and link references were stripped inside + fenced code blocks. + +* Fixed a bug where characters such as `"` in abbreviation + definitions weren't properly encoded to HTML entities. + +* Fixed a bug where double quotes `"` were not correctly encoded + as HTML entities when used inside a footnote reference id. + + +1.0.1m (21 Jun 2008): + +* Lists can now have empty items. + +* Rewrote the emphasis and strong emphasis parser to fix some issues + with odly placed and overlong markers. + + +Extra 1.2.1 (27 May 2008): + +* Fixed a problem where Markdown headers and horizontal rules were + transformed into their HTML equivalent inside fenced code blocks. + + +Extra 1.2 (11 May 2008): + +* Added fenced code block syntax which don't require indentation + and can start and end with blank lines. A fenced code block + starts with a line of consecutive tilde (~) and ends on the + next line with the same number of consecutive tilde. Here's an + example: + + ~~~~~~~~~~~~ + Hello World! + ~~~~~~~~~~~~ + +* Rewrote parts of the HTML block parser to better accomodate + fenced code blocks. + +* Footnotes may now be referenced from within another footnote. + +* Added programatically-settable parser property `predef_attr` for + predefined attribute definitions. + +* Fixed an issue where an indented code block preceded by a blank + line containing some other whitespace would confuse the HTML + block parser into creating an HTML block when it should have + been code. + + +1.0.1l (11 May 2008): + +* Now removing the UTF-8 BOM at the start of a document, if present. + +* Now accepting capitalized URI schemes (such as HTTP:) in automatic + links, such as ``. + +* Fixed a problem where `` was seen as a horizontal + rule instead of an automatic link. + +* Fixed an issue where some characters in Markdown-generated HTML + attributes weren't properly escaped with entities. + +* Fix for code blocks as first element of a list item. Previously, + this didn't create any code block for item 2: + + * Item 1 (regular paragraph) + + * Item 2 (code block) + +* A code block starting on the second line of a document wasn't seen + as a code block. This has been fixed. + +* Added programatically-settable parser properties `predef_urls` and + `predef_titles` for predefined URLs and titles for reference-style + links. To use this, your PHP code must call the parser this way: + + $parser = new Markdwon_Parser; + $parser->predef_urls = array('linkref' => 'http://example.com'); + $html = $parser->transform($text); + + You can then use the URL as a normal link reference: + + [my link][linkref] + [my link][linkRef] + + Reference names in the parser properties *must* be lowercase. + Reference names in the Markdown source may have any case. + +* Added `setup` and `teardown` methods which can be used by subclassers + as hook points to arrange the state of some parser variables before and + after parsing. + + +Extra 1.1.7 (26 Sep 2007): + +1.0.1k (26 Sep 2007): + +* Fixed a problem introduced in 1.0.1i where three or more identical + uppercase letters, as well as a few other symbols, would trigger + a horizontal line. + + +Extra 1.1.6 (4 Sep 2007): + +1.0.1j (4 Sep 2007): + +* Fixed a problem introduced in 1.0.1i where the closing `code` and + `pre` tags at the end of a code block were appearing in the wrong + order. + +* Overriding configuration settings by defining constants from an + external before markdown.php is included is now possible without + producing a PHP warning. + + +Extra 1.1.5 (31 Aug 2007): + +1.0.1i (31 Aug 2007): + +* Fixed a problem where an escaped backslash before a code span + would prevent the code span from being created. This should now + work as expected: + + Litteral backslash: \\`code span` + +* Overall speed improvements, especially with long documents. + + +Extra 1.1.4 (3 Aug 2007): + +1.0.1h (3 Aug 2007): + +* Added two properties (`no_markup` and `no_entities`) to the parser + allowing HTML tags and entities to be disabled. + +* Fix for a problem introduced in 1.0.1g where posting comments in + WordPress would trigger PHP warnings and cause some markup to be + incorrectly filtered by the kses filter in WordPress. + + +Extra 1.1.3 (3 Jul 2007): + +* Fixed a performance problem when parsing some invalid HTML as an HTML + block which was resulting in too much recusion and a segmentation fault + for long documents. + +* The markdown="" attribute now accepts unquoted values. + +* Fixed an issue where underscore-emphasis didn't work when applied on the + first or the last word of an element having the markdown="1" or + markdown="span" attribute set unless there was some surrounding whitespace. + This didn't work: + +

    _Hello_ _world_

    + + Now it does produce emphasis as expected. + +* Fixed an issue preventing footnotes from working when the parser's + footnote id prefix variable (fn_id_prefix) is not empty. + +* Fixed a performance problem where the regular expression for strong + emphasis introduced in version 1.1 could sometime be long to process, + give slightly wrong results, and in some circumstances could remove + entirely the content for a whole paragraph. + +* Fixed an issue were abbreviations tags could be incorrectly added + inside URLs and title of links. + +* Placing footnote markers inside a link, resulting in two nested links, is + no longer allowed. + + +1.0.1g (3 Jul 2007): + +* Fix for PHP 5 compiled without the mbstring module. Previous fix to + calculate the length of UTF-8 strings in `detab` when `mb_strlen` is + not available was only working with PHP 4. + +* Fixed a problem with WordPress 2.x where full-content posts in RSS feeds + were not processed correctly by Markdown. + +* Now supports URLs containing literal parentheses for inline links + and images, such as: + + [WIMP](http://en.wikipedia.org/wiki/WIMP_(computing)) + + Such parentheses may be arbitrarily nested, but must be + balanced. Unbalenced parentheses are allowed however when the URL + when escaped or when the URL is enclosed in angle brakets `<>`. + +* Fixed a performance problem where the regular expression for strong + emphasis introduced in version 1.0.1d could sometime be long to process, + give slightly wrong results, and in some circumstances could remove + entirely the content for a whole paragraph. + +* Some change in version 1.0.1d made possible the incorrect nesting of + anchors within each other. This is now fixed. + +* Fixed a rare issue where certain MD5 hashes in the content could + be changed to their corresponding text. For instance, this: + + The MD5 value for "+" is "26b17225b626fb9238849fd60eabdf60". + + was incorrectly changed to this in previous versions of PHP Markdown: + +

    The MD5 value for "+" is "+".

    + +* Now convert escaped characters to their numeric character + references equivalent. + + This fix an integration issue with SmartyPants and backslash escapes. + Since Markdown and SmartyPants have some escapable characters in common, + it was sometime necessary to escape them twice. Previously, two + backslashes were sometime required to prevent Markdown from "eating" the + backslash before SmartyPants sees it: + + Here are two hyphens: \\-- + + Now, only one backslash will do: + + Here are two hyphens: \-- + + +Extra 1.1.2 (7 Feb 2007) + +* Fixed an issue where headers preceded too closely by a paragraph + (with no blank line separating them) where put inside the paragraph. + +* Added the missing TextileRestricted method that was added to regular + PHP Markdown since 1.0.1d but which I forgot to add to Extra. + + +1.0.1f (7 Feb 2007): + +* Fixed an issue with WordPress where manually-entered excerpts, but + not the auto-generated ones, would contain nested paragraphs. + +* Fixed an issue introduced in 1.0.1d where headers and blockquotes + preceded too closely by a paragraph (not separated by a blank line) + where incorrectly put inside the paragraph. + +* Fixed an issue introduced in 1.0.1d in the tokenizeHTML method where + two consecutive code spans would be merged into one when together they + form a valid tag in a multiline paragraph. + +* Fixed an long-prevailing issue where blank lines in code blocks would + be doubled when the code block is in a list item. + + This was due to the list processing functions relying on artificially + doubled blank lines to correctly determine when list items should + contain block-level content. The list item processing model was thus + changed to avoid the need for double blank lines. + +* Fixed an issue with `<% asp-style %>` instructions used as inline + content where the opening `<` was encoded as `<`. + +* Fixed a parse error occuring when PHP is configured to accept + ASP-style delimiters as boundaries for PHP scripts. + +* Fixed a bug introduced in 1.0.1d where underscores in automatic links + got swapped with emphasis tags. + + +Extra 1.1.1 (28 Dec 2006) + +* Fixed a problem where whitespace at the end of the line of an atx-style + header would cause tailing `#` to appear as part of the header's content. + This was caused by a small error in the regex that handles the definition + for the id attribute in PHP Markdown Extra. + +* Fixed a problem where empty abbreviations definitions would eat the + following line as its definition. + +* Fixed an issue with calling the Markdown parser repetitivly with text + containing footnotes. The footnote hashes were not reinitialized properly. + + +1.0.1e (28 Dec 2006) + +* Added support for internationalized domain names for email addresses in + automatic link. Improved the speed at which email addresses are converted + to entities. Thanks to Milian Wolff for his optimisations. + +* Made deterministic the conversion to entities of email addresses in + automatic links. This means that a given email address will always be + encoded the same way. + +* PHP Markdown will now use its own function to calculate the length of an + UTF-8 string in `detab` when `mb_strlen` is not available instead of + giving a fatal error. + + +Extra 1.1 (1 Dec 2006) + +* Added a syntax for footnotes. + +* Added an experimental syntax to define abbreviations. + + +1.0.1d (1 Dec 2006) + +* Fixed a bug where inline images always had an empty title attribute. The + title attribute is now present only when explicitly defined. + +* Link references definitions can now have an empty title, previously if the + title was defined but left empty the link definition was ignored. This can + be useful if you want an empty title attribute in images to hide the + tooltip in Internet Explorer. + +* Made `detab` aware of UTF-8 characters. UTF-8 multi-byte sequences are now + correctly mapped to one character instead of the number of bytes. + +* Fixed a small bug with WordPress where WordPress' default filter `wpautop` + was not properly deactivated on comment text, resulting in hard line breaks + where Markdown do not prescribes them. + +* Added a `TextileRestrited` method to the textile compatibility mode. There + is no restriction however, as Markdown does not have a restricted mode at + this point. This should make PHP Markdown work again in the latest + versions of TextPattern. + +* Converted PHP Markdown to a object-oriented design. + +* Changed span and block gamut methods so that they loop over a + customizable list of methods. This makes subclassing the parser a more + interesting option for creating syntax extensions. + +* Also added a "document" gamut loop which can be used to hook document-level + methods (like for striping link definitions). + +* Changed all methods which were inserting HTML code so that they now return + a hashed representation of the code. New methods `hashSpan` and `hashBlock` + are used to hash respectivly span- and block-level generated content. This + has a couple of significant effects: + + 1. It prevents invalid nesting of Markdown-generated elements which + could occur occuring with constructs like `*something [link*][1]`. + 2. It prevents problems occuring with deeply nested lists on which + paragraphs were ill-formed. + 3. It removes the need to call `hashHTMLBlocks` twice during the the + block gamut. + + Hashes are turned back to HTML prior output. + +* Made the block-level HTML parser smarter using a specially-crafted regular + expression capable of handling nested tags. + +* Solved backtick issues in tag attributes by rewriting the HTML tokenizer to + be aware of code spans. All these lines should work correctly now: + + bar + bar + `` + +* Changed the parsing of HTML comments to match simply from `` + instead using of the more complicated SGML-style rule with paired `--`. + This is how most browsers parse comments and how XML defines them too. + +* `
    ` has been added to the list of block-level elements and is now + treated as an HTML block instead of being wrapped within paragraph tags. + +* Now only trim trailing newlines from code blocks, instead of trimming + all trailing whitespace characters. + +* Fixed bug where this: + + [text](http://m.com "title" ) + + wasn't working as expected, because the parser wasn't allowing for spaces + before the closing paren. + +* Filthy hack to support markdown='1' in div tags. + +* _DoAutoLinks() now supports the 'dict://' URL scheme. + +* PHP- and ASP-style processor instructions are now protected as + raw HTML blocks. + + + <% ... %> + +* Fix for escaped backticks still triggering code spans: + + There are two raw backticks here: \` and here: \`, not a code span + + +Extra 1.0 - 5 September 2005 + +* Added support for setting the id attributes for headers like this: + + Header 1 {#header1} + ======== + + ## Header 2 ## {#header2} + + This only work only for headers for now. + +* Tables will now work correctly as the first element of a definition + list. For example, this input: + + Term + + : Header | Header + ------- | ------- + Cell | Cell + + used to produce no definition list and a table where the first + header was named ": Header". This is now fixed. + +* Fix for a problem where a paragraph following a table was not + placed between `

    ` tags. + + +Extra 1.0b4 - 1 August 2005 + +* Fixed some issues where whitespace around HTML blocks were trigging + empty paragraph tags. + +* Fixed an HTML block parsing issue that would cause a block element + following a code span or block with unmatched opening bracket to be + placed inside a paragraph. + +* Removed some PHP notices that could appear when parsing definition + lists and tables with PHP notice reporting flag set. + + +Extra 1.0b3 - 29 July 2005 + +* Definition lists now require a blank line before each term. Solves + an ambiguity where the last line of lazy-indented definitions could + be mistaken by PHP Markdown as a new term in the list. + +* Definition lists now support multiple terms per definition. + +* Some special tags were replaced in the output by their md5 hash + key. Things such as this now work as expected: + + ## Header ## + + +Extra 1.0b2 - 26 July 2005 + +* Definition lists can now take two or more definitions for one term. + This should have been the case before, but a bug prevented this + from working right. + +* Fixed a problem where single column table with a pipe only at the + end where not parsed as table. Here is such a table: + + | header + | ------ + | cell + +* Fixed problems with empty cells in the first column of a table with + no leading pipe, like this one: + + header | header + ------ | ------ + | cell + +* Code spans containing pipes did not within a table. This is now + fixed by parsing code spans before splitting rows into cells. + +* Added the pipe character to the backlash escape character lists. + +Extra 1.0b1 (25 Jun 2005) + +* First public release of PHP Markdown Extra. + + +Copyright and License +--------------------- + +PHP Markdown & Extra +Copyright (c) 2004-2009 Michel Fortin + +All rights reserved. + +Based on Markdown +Copyright (c) 2003-2005 John Gruber + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the + distribution. + +* Neither the name "Markdown" nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +This software is provided by the copyright holders and contributors "as +is" and any express or implied warranties, including, but not limited +to, the implied warranties of merchantability and fitness for a +particular purpose are disclaimed. In no event shall the copyright owner +or contributors be liable for any direct, indirect, incidental, special, +exemplary, or consequential damages (including, but not limited to, +procurement of substitute goods or services; loss of use, data, or +profits; or business interruption) however caused and on any theory of +liability, whether in contract, strict liability, or tort (including +negligence or otherwise) arising in any way out of the use of this +software, even if advised of the possibility of such damage. diff --git a/docs/nav.php b/docs/nav.php new file mode 100644 index 0000000..e90e3b9 --- /dev/null +++ b/docs/nav.php @@ -0,0 +1,8 @@ +

    \ No newline at end of file diff --git a/docs/styles/elements.less b/docs/styles/elements.less new file mode 100755 index 0000000..ffda4f1 --- /dev/null +++ b/docs/styles/elements.less @@ -0,0 +1,142 @@ +/*--------------------------------------------------- + LESS Elements 0.6 + --------------------------------------------------- + A set of useful LESS mixins by Dmitry Fadeyev + Special thanks for mixin suggestions to: + Kris Van Herzeele, + Benoit Adam, + Portenart Emile-Victor, + Ryan Faerman + + More info at: http://lesselements.com +-----------------------------------------------------*/ + +.gradient(@color: #F5F5F5, @start: #EEE, @stop: #FFF) { + background: @color; + background: -webkit-gradient(linear, + left bottom, + left top, + color-stop(0, @start), + color-stop(1, @stop)); + background: -ms-linear-gradient(bottom, + @start, + @stop); + background: -moz-linear-gradient(center bottom, + @start 0%, + @stop 100%); +} +.bw-gradient(@color: #F5F5F5, @start: 0, @stop: 255) { + background: @color; + background: -webkit-gradient(linear, + left bottom, + left top, + color-stop(0, rgb(@start,@start,@start)), + color-stop(1, rgb(@stop,@stop,@stop))); + background: -ms-linear-gradient(bottom, + rgb(@start,@start,@start) 0%, + rgb(@start,@start,@start) 100%); + background: -moz-linear-gradient(center bottom, + rgb(@start,@start,@start) 0%, + rgb(@stop,@stop,@stop) 100%); +} +.bordered(@top-color: #EEE, @right-color: #EEE, @bottom-color: #EEE, @left-color: #EEE) { + border-top: solid 1px @top-color; + border-left: solid 1px @left-color; + border-right: solid 1px @right-color; + border-bottom: solid 1px @bottom-color; +} +.drop-shadow(@x-axis: 0, @y-axis: 1px, @blur: 2px, @alpha: 0.1) { + -webkit-box-shadow: @x-axis @y-axis @blur rgba(0, 0, 0, @alpha); + -moz-box-shadow: @x-axis @y-axis @blur rgba(0, 0, 0, @alpha); + box-shadow: @x-axis @y-axis @blur rgba(0, 0, 0, @alpha); +} +.rounded(@radius: 2px) { + -webkit-border-radius: @radius; + -moz-border-radius: @radius; + border-radius: @radius; + -moz-background-clip: padding; -webkit-background-clip: padding-box; background-clip: padding-box; +} +.border-radius(@topright: 0, @bottomright: 0, @bottomleft: 0, @topleft: 0) { + -webkit-border-top-right-radius: @topright; + -webkit-border-bottom-right-radius: @bottomright; + -webkit-border-bottom-left-radius: @bottomleft; + -webkit-border-top-left-radius: @topleft; + -moz-border-radius-topright: @topright; + -moz-border-radius-bottomright: @bottomright; + -moz-border-radius-bottomleft: @bottomleft; + -moz-border-radius-topleft: @topleft; + border-top-right-radius: @topright; + border-bottom-right-radius: @bottomright; + border-bottom-left-radius: @bottomleft; + border-top-left-radius: @topleft; + -moz-background-clip: padding; -webkit-background-clip: padding-box; background-clip: padding-box; +} +.opacity(@opacity: 0.5) { + -moz-opacity: @opacity; + -khtml-opacity: @opacity; + -webkit-opacity: @opacity; + opacity: @opacity; +} +.transition-duration(@duration: 0.2s) { + -moz-transition-duration: @duration; + -webkit-transition-duration: @duration; + transition-duration: @duration; +} +.rotation(@deg:5deg){ + -webkit-transform: rotate(@deg); + -moz-transform: rotate(@deg); + transform: rotate(@deg); +} +.scale(@ratio:1.5){ + -webkit-transform:scale(@ratio); + -moz-transform:scale(@ratio); + transform:scale(@ratio); +} +.transition(@duration:0.2s, @ease:ease-out, @prop:all) { + -webkit-transition: @prop @duration @ease; + -moz-transition: @prop @duration @ease; + transition: @prop @duration @ease; +} +.transition2(@d1:0.2s, @e1:ease-out, @p1:all, + @d2:0.2s, @e2:ease-out, @p2:all) { + -webkit-transition: @d1 @e1 @p1, @d2 @e2 @p2; + -moz-transition: @d1 @e1 @p1, @d2 @e2 @p2; + transition: @d1 @e1 @p1, @d2 @e2 @p2; +} +.inner-shadow(@horizontal:0, @vertical:1px, @blur:2px, @alpha: 0.4) { + -webkit-box-shadow: inset @horizontal @vertical @blur rgba(0, 0, 0, @alpha); + -moz-box-shadow: inset @horizontal @vertical @blur rgba(0, 0, 0, @alpha); + box-shadow: inset @horizontal @vertical @blur rgba(0, 0, 0, @alpha); +} +.box-shadow(@arguments) { + -webkit-box-shadow: @arguments; + -moz-box-shadow: @arguments; + box-shadow: @arguments; +} +.columns(@colwidth: 250px, @colcount: 0, @colgap: 50px, @columnRuleColor: #EEE, @columnRuleStyle: solid, @columnRuleWidth: 1px) { + -moz-column-width: @colwidth; + -moz-column-count: @colcount; + -moz-column-gap: @colgap; + -moz-column-rule-color: @columnRuleColor; + -moz-column-rule-style: @columnRuleStyle; + -moz-column-rule-width: @columnRuleWidth; + -webkit-column-width: @colwidth; + -webkit-column-count: @colcount; + -webkit-column-gap: @colgap; + -webkit-column-rule-color: @columnRuleColor; + -webkit-column-rule-style: @columnRuleStyle; + -webkit-column-rule-width: @columnRuleWidth; + column-width: @colwidth; + column-count: @colcount; + column-gap: @colgap; + column-rule-color: @columnRuleColor; + column-rule-style: @columnRuleStyle; + column-rule-width: @columnRuleWidth; +} +.translate(@x:0, @y:0) { + -moz-transform: translate(@x, @y); + -webkit-transform: translate(@x, @y); + -o-transform: translate(@x, @y); + -ms-transform: translate(@x, @y); + transform: translate(@x, @y); +} diff --git a/docs/styles/presto.css b/docs/styles/presto.css new file mode 100644 index 0000000..8828069 --- /dev/null +++ b/docs/styles/presto.css @@ -0,0 +1,20 @@ +body{font:115%/150% "freight-sans-pro","Helvetica Neau",Helvetica;}body header,body section,body footer{width:100%;position:relative;} +body header>div,body section>div,body footer>div{position:relative;width:40em;margin:0 auto;} +body>a{position:fixed;right:0;top:0;z-index:999;display:block;width:125px;height:125px;} +body a,body a:visited,body a:hover{color:#963232;text-decoration:none;} +body h1,body h2,body h4,body h5{margin:2em 0 0;} +body h3{margin:1.75em 0 0;font-size:smaller;font-weight:400;text-transform:uppercase;} +body code,body pre{font:10pt/100% Menlo,Monaco,"Andale Mono",monospace;} +body pre{background-color:rgba(0, 0, 0, 0.1);padding:.75em 1em;-webkit-border-radius:0.5em;-moz-border-radius:0.5em;border-radius:0.5em;-moz-background-clip:padding;-webkit-background-clip:padding-box;background-clip:padding-box;overflow:hidden;} +body pre.highlight{border:0 !important;padding:.75em !important;width:97% !important;font:10pt/100% Menlo,Monaco,"Andale Mono",monospace !important;} +body p{margin:.25em 0 1em;} +body p>code{background-color:rgba(0, 0, 0, 0.1);padding:.35em .25em;margin:0 .25em 0 0;-webkit-border-radius:0.25em;-moz-border-radius:0.25em;border-radius:0.25em;-moz-background-clip:padding;-webkit-background-clip:padding-box;background-clip:padding-box;} +body header h1{margin:0 0 .5em;}body header h1 em{display:block;font-size:10pt;font-style:normal;font-weight:lighter;color:#333;} +body header nav{position:absolute;right:0;top:.5em;width:auto;}body header nav a{display:block;float:left;width:5em;line-height:95%;margin-right:1em;padding-bottom:.25em;font-size:11pt;font-weight:lighter;text-align:center;text-transform:uppercase;border-bottom:4px solid transparent;} +body header nav a:hover{border-color:#963232;} +body footer div{border-top:1px solid rgba(0, 0, 0, 0.25);margin-top:3em;}body footer div h5{font-weight:200;} +body details:nth-child(1){margin-top:2em;} +body details{border:1px solid rgba(0, 0, 0, 0.25);padding:.25em .5em;margin:.25em .5em;}body details summary::-webkit-details-marker{content:'...';} +body details summary{position:relative;font-weight:bold;outline:none;cursor:pointer;}body details summary var{position:absolute;right:0;top:0;width:3em;text-align:center;font-style:normal;font-weight:bold;background-color:rgba(0, 0, 0, 0.25);color:rgba(0, 0, 0, 0.5);} +body details summary var.fail,body details summary var.off{background-color:#963232;color:#fff;} +body details summary var.pass,body details summary var.on{background-color:green;color:#fff;} diff --git a/docs/styles/presto.less b/docs/styles/presto.less new file mode 100644 index 0000000..dcf9f52 --- /dev/null +++ b/docs/styles/presto.less @@ -0,0 +1,148 @@ +@import 'elements.less'; + +@hl: rgba(150, 50, 50, 100); +@bad: rgba(150, 50, 50, 100); +@good: rgba(50, 150, 50, 100); +@light: rgba(0,0,0,.25); +@med: rgba(0,0,0,.5); +@shadow: rgba(0,0,0,.75); + +body { + font: 115%/150% "freight-sans-pro", "Helvetica Neau", Helvetica; + + /* Page layout, grid, and colours */ + header, section, footer { + width: 100%; + position: relative; + } + header>div, section>div, footer>div { + position: relative; + width: 40em; + margin: 0 auto; + } + + /* Overrides */ + + > a { + position: fixed; right: 0; top: 0; z-index: 999; + display: block; + width: 125px; height: 125px; + } + a, a:visited, a:hover { + color: @hl; + text-decoration: none; + } + h1, h2, h4, h5 { + margin: 2em 0 0; + } + h3 { + margin: 1.75em 0 0; + font-size: smaller; + font-weight: 400; + text-transform: uppercase; + } + + code, pre { + font: 10pt/100% Menlo, Monaco, "Andale Mono", monospace; + } + pre { + background-color: rgba(0,0,0,.1); + padding: .75em 1em; + .rounded(.5em); + overflow: hidden; + } + pre.highlight { + border: 0 !important; + padding: .75em !important; + width: 97% !important; + font: 10pt/100% Menlo, Monaco, "Andale Mono", monospace !important; + } + + p { + margin: .25em 0 1em; + } + p>code { + background-color: rgba(0,0,0,.1); + padding: .35em .25em; margin: 0 .25em 0 0; + .rounded(.25em); + } + + + /* Sections */ + + header { + h1 { + margin: 0 0 .5em; + + em { + display: block; + font-size: 10pt; + font-style: normal; + font-weight: lighter; + color: #333; + } + } + + nav { + position: absolute; right: 0; top: .5em; + width: auto; + + a { + display: block; float: left; + width: 5em; line-height: 95%; + margin-right: 1em; padding-bottom: .25em; + font-size: 11pt; + font-weight: lighter; + text-align: center; + text-transform: uppercase; + border-bottom: 4px solid transparent; + } + a:hover { + border-color: @hl; + } + } + } + + footer { + div { + border-top: 1px solid @light; + margin-top: 3em; + + h5 { + font-weight: 200; + } + } + } + + /* Test blocks */ + + details:nth-child(1) { + margin-top: 2em; + } + + details { + border: 1px solid rgba(0,0,0,.25); + padding: .25em .5em; margin: .25em .5em; + + summary::-webkit-details-marker { content: '...'; } + + summary { + position: relative; + font-weight: bold; + outline: none; + cursor: pointer; + + var { + position: absolute; right: 0; top: 0; + width: 3em; + text-align: center; + font-style: normal; + font-weight: bold; + background-color: @light; color: @med; + } + var.fail, var.off { background-color: @bad; color: #fff; } + var.pass, var.on { background-color: green; color: #fff; } + } + } + +} \ No newline at end of file diff --git a/docs/tutorial.php b/docs/tutorial.php new file mode 100644 index 0000000..61520e3 --- /dev/null +++ b/docs/tutorial.php @@ -0,0 +1,16 @@ + +http://presto.napkinware.com/docs/example.json + +
    +
    +	
    +
    +	
    +
    + +
    +
    +	
    +
    +	
    +
    \ No newline at end of file diff --git a/examples/.htaccess b/examples/.htaccess deleted file mode 120000 index 0ce9f30..0000000 --- a/examples/.htaccess +++ /dev/null @@ -1 +0,0 @@ -../lib/htaccess-example \ No newline at end of file diff --git a/examples/index.php b/examples/index.php deleted file mode 120000 index 8bae94b..0000000 --- a/examples/index.php +++ /dev/null @@ -1 +0,0 @@ -../lib/install-checklist.php \ No newline at end of file diff --git a/lib/_config.php b/lib/_config.php index 9fb5f0d..7593497 100644 --- a/lib/_config.php +++ b/lib/_config.php @@ -1,10 +1,12 @@ -line = $line; $e->file = $file; throw $e; } -} +} // Set up PHP error handling (note these settings are overriden by explicit PHP ini settigns, we should address this) assert_options(ASSERT_WARNING, 0); +assert_options(ASSERT_QUIET_EVAL, 1); +if (PRESTO_DEBUG) { + assert_options(ASSERT_ACTIVE, 1); + assert_options(ASSERT_WARNING, 1); + assert_options(ASSERT_BAIL, 0); +} ini_set('html_errors', false); error_reporting(E_ALL); set_error_handler(array("PrestoException", "errorHandlerCallback"), E_ALL); +// Create a handler function +function presto_assert_handler($file, $line, $code, $description = 'no description available') { + if (empty($code)) $code = 0; + $message = "Assert failed in $file:$line with #$code - $description."; + error_log($message); + PrestoException::errorHandlerCallback(500, $message, $file, $line, NULL); +} + +// Register Presto assert handling +assert_options(ASSERT_CALLBACK, 'presto_assert_handler'); + diff --git a/lib/_helpers.php b/lib/_helpers.php index 7f68f50..0756ad3 100644 --- a/lib/_helpers.php +++ b/lib/_helpers.php @@ -1,9 +1,24 @@ -concepts[] = $concept; - } + if (func_num_args() == 2) throw new Exception('Code upgrade required (Presto base classes have changed).', 500); } + /* Attach to Presto framework */ - public function attach($ctx, $resp, $req) { + public static function attach($ctx, $resp, $req) { self::$ctx = $ctx; self::$resp = $resp; self::$req = $req; @@ -49,53 +38,32 @@ public function add_header($key, $value) { $this->headers[$key] = $value; } public function headers() { return $this->headers; } - - /* Test if a route refers to a valid concept (member) */ - public function is_valid_concept($c) { return !empty($this->concepts) - && in_array($c, $this->concepts); } - - /** Advanced route mapping - - Adds a mapping between a URI pattern and a callback to delegate to. Used to route complex - sub delegates, for things like hierarchical resources. - */ - public function add_delegate($regex, $delegateFn) { - - // check for conflicts in previously added delegates. - if (array_key_exists($regex, $this->delegates)) - throw new Exception("URI delegate already exists: pattern collision for '$regex'", 500); - - // preflight (and compile + cache) the regex ... errors handled by Presto. - if (!preg_match($regex, '')) $this->delegates[$regex] = $delegateFn; + + /* Restrict the valid contentTypes for this API or API route */ + public function restrictTo($types) { + return $this->supports_contentType($types); } + + /* Set necessary CORS headers for this API route. - /* Do delegation for hierarchical sub-routes - - Provides internal delegation to registered callbacks. - - Throws if delegation fails. + * Sets `Access-Control-Allow-Origin` appropriately with respect to referer (necessary for `Access-Control-Allow-Credentials`) + * Sets `Access-Control-Allow-Credentials: true`: allow `rx-auth` cookie to be included in request (authentication) + * Sets `Access-Control-Allow-Methods: GET`: only GETs supported for now + * sets some other useful default headers + */ - public function delegate($ctx, $data = null) { - if (empty($this->delegates) || empty($ctx) || empty($ctx->params)) - throw new Exception('Unserviceable internal delegation attempt.', 501); + public function allowCrossOrigin() { + + if (empty($_SERVER['HTTP_ORIGIN'])) + return; // This is not a CORS request - $path = implode('/', $ctx->params); - foreach ($this->delegates as $p => $d) { - if (preg_match($p, $path)) { - if (empty($data)) return $this->$d($ctx); - else return $this->$d($ctx, $data); - } - } - throw new Exception("Bad request. No sub method exists for resource $path", 404); + $this->add_header('Access-Control-Allow-Origin', $_SERVER['HTTP_ORIGIN']); + $this->add_header('Access-Control-Allow-Credentials', 'true'); + $this->add_header('Access-Control-Allow-Methods', 'GET'); + $this->add_header('Access-Control-Allow-Headers', 'Content-Type,Accept'); + $this->add_header('Access-Control-Max-Age', '10'); } - - /* Restrict the valid contentTypes for this API or API route - - */ - public function restrictTo($types) { - return $this->validate_contentType($types); - } - + /* Get a filtered variable (get filter_var_array + exceptions) */ static function filtered($thing, $rules, $defaults = null) { if ($defaults) $thing = array_merge($defaults, (array)$thing); @@ -106,18 +74,18 @@ static function filtered($thing, $rules, $defaults = null) { if ( $missing = array_filter( $filtered, function($v) { return $v === null; } ) ) throw new Exception("Missing parameter(s): " . implode(array_keys($missing), ', '), 406); - + if ( $invalid = array_filter( $filtered, function($v) { return is_bool($v) && $v === FALSE; } ) ) throw new Exception("Invalid parameter(s): " . implode(array_keys($invalid), ', '), 406); return (object) $filtered; } - - private function validate_contentType($t) { + + private function supports_contentType($t) { $in = self::$ctx->class . '::' . self::$ctx->method . '()'; - $res = self::$ctx->res; - - if (!is_array($t)) $t = array($t); - if (!in_array($res, $t)) throw new Exception("Unsupported media type $res for $in.", 415); + $type = self::$ctx->type; + + if (!is_array($t)) $t = array($t); + if (!in_array($type, $t)) throw new Exception("Unsupported media type '$type' for '$in'.", 415); } } diff --git a/lib/auth_token.php b/lib/auth_token.php index 74957c2..3b0c583 100644 --- a/lib/auth_token.php +++ b/lib/auth_token.php @@ -1,5 +1,5 @@ -build($v, false /* force creation of check + timestamp */ ); - $this->encrypt(); + $this->encrypt(); } elseif (is_string($v)) { // build from encrypted string $this->t = $v; - $this->decrypt(); + $this->decrypt(); } else throw new Exception('Missing auth_token or token parts.', 500); } - + /* Get the encoded token */ public function encoded() { $this->encrypt(); return $this->t; } - + /* Get the token parts */ public function parts() {return $this->p;} - + /* Is this token valid? */ public function ok() { - return count($this->p) && !empty($this->p->h) + return count($this->p) && !empty($this->p->h) && $this->p->h == $this->hash; } - - /* ---------------------------- internals ---------------------------- */ - - /* Get the checked parts as a string (for calculating checksums) */ + + /* Get the checked parts as a string (for calculating checksums) */ private function checked_parts() { if (empty($this->p)) throw new Exception('Token is not initialized.', 500); - $elements = array($this->p->name, $this->p->email, $this->p->id, $this->p->acct, - TOKEN_HASH_SECRET, $this->p->t); - + $elements = array($this->p->name, $this->p->email, $this->p->id, $this->p->acct, + TOKEN_HASH_SECRET, $this->p->t); + foreach ($elements as &$e) $e = urlencode($e); return implode('', $elements); } - /* Get a token as an encoded URI string */ + /* Get a token as an encoded URI string */ private function uri() { if (empty($this->p)) throw new Exception('Token is not initialized.', 500); $uri = ''; foreach ($this->p as $k => $v) $uri .= $k.'='.urlencode($v).'&'; - return $uri; + return $uri; } - - /* Build the token object from parts - + + /* Build the token object from parts + A strict build checks that all parts are provided - + Returns false if the token build has failed. */ private function build($p, $strict = true) { // apply defaults - $this->set($p['t'], time(), $strict); - $this->set($p['w'], 60, $strict); + if (!array_key_exists('t', $p)) $this->set($p['t'], time(), $strict); + if (!array_key_exists('w', $p)) $this->set($p['w'], 60, $strict); // check for required elements - foreach (array('name', 'id', 'acct', 'a', 'c', 's') as $k) + foreach (array('name', 'id', 'acct', 'a', 's') as $k) if (empty($p[$k])) // missing a required token element throw new Exception('Invalid credentials, missing: '.$k, 401); - + if (empty($p['email']) && empty($p['key'])) throw new Exception('Invalid credentials, missing email and key, at least one is required', 401); foreach ($p as $k => &$v) $v = urldecode($v); // remove URI encoding - + $this->p = (object) $p; // objectize for convinience // extract capabilities+roles - if (strlen($this->p->c)) { + if (!empty($this->p->c) && strlen($this->p->c)) { $roles = explode(',', $this->p->c); foreach ($roles as $tuple) { $cap = explode('/', $tuple); @@ -110,37 +108,37 @@ private function build($p, $strict = true) { $this->roles[$role] = array_keys($list); } } - + // lastly, apply a default value to the CRC (if needed/possible) - $this->set($this->p->h, sha1($this->checked_parts()), $strict); + $this->set($this->p->h, sha1($this->checked_parts()), $strict); $this->hash = sha1($this->checked_parts()); - + return $this->hash == $this->p->h; } - - /* Assign a token value a default (if possible) */ + + /* Assign a token value a default (if possible) */ private function set(&$v, $d, $strict = true) { if (!empty($v)) return; - - if (!$strict) $v = $d; + + if (!$strict) $v = $d; else throw new Exception('Missing required check field.', 401); } - + /* Encrypt a token from parts */ - private function encrypt() { - $this->t = openssl_encrypt($this->uri(), + private function encrypt() { + $this->t = openssl_encrypt($this->uri(), TOKEN_ENCRYPTION, TOKEN_SIGNING_KEY, false, SIGNING_INIT); - + return $this->t; } - + /* Decrypt a token into parts (thows on errors) */ - private function decrypt() { + private function decrypt() { if (empty($this->t)) throw new Exception('Token is empty.', 401); - $t = openssl_decrypt($this->t, + $t = openssl_decrypt($this->t, TOKEN_ENCRYPTION, TOKEN_SIGNING_KEY, false, SIGNING_INIT); if (empty($t)) @@ -150,10 +148,10 @@ private function decrypt() { if (empty($p)) throw new Exception('Token format invalid.', 401); - + if (!$this->build( $p )) - throw new Exception('Token integrity check failed.', 401); - + throw new Exception('Token integrity check failed.', 401); + return $this->p; } diff --git a/lib/autoloader.php b/lib/autoloader.php new file mode 100644 index 0000000..fef7192 --- /dev/null +++ b/lib/autoloader.php @@ -0,0 +1,50 @@ +container; + $error = "API `$call->class` not found"; + + if ( !stream_resolve_include_path($call->file) ) { + $extra = " ({$call->file} not found)."; + + if ( !empty($in) && !is_dir($in) ) + $extra = " ({$call->file} not found, $in missing)."; // aid debugging of routes-in-folders + + throw new Exception($error . $extra, 404); + } + + if ( !(require_once $call->file) ) + throw new Exception($error . " ({$call->file} not loadable).", 500); + + +} diff --git a/lib/db.php b/lib/db.php index c7af506..946bfd0 100644 --- a/lib/db.php +++ b/lib/db.php @@ -5,95 +5,334 @@ See PDO docs for details: http://www.php.net/manual/en/class.pdo.php Adds a few extensions for common batched operations. - + See related `extra/tagged-sql.php` for extra post-processing magic. */ class db extends PDO { - private $statement; - const USR_DEF_DB_ERR = '45000'; + private $statement; + private static $valid_pdo_types = array(PDO::PARAM_INT, PDO::PARAM_NULL, + PDO::PARAM_BOOL, PDO::PARAM_STR); + const USR_DEF_DB_ERR = '45000'; const DB_NO_ERR = '00000'; + + /* Create (or reuse) an instance of a PDO database */ - static function _instance($dsn, $user, $password, $config = null) { - global $_db; - if ($_db !== null) return $_db; // return cached - - if ($config === null) - $config = array( PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8' ); - - try { - $_db = new db($dsn, $user, $password, $config); - } catch (Exception $e) { + static function _instance($dsn = null, $user = null, $password = null, $config = null) { + global $_db; + if ($_db !== null) return $_db; // return cached + + if ($config === null) + $config = array( PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8' ); + + try { + $_db = new db($dsn, $user, $password, $config); + $_db->setAttribute(constant("PDO::ATTR_ERRMODE"), constant("PDO::ERRMODE_EXCEPTION")); + } catch (Exception $e) { throw new Exception("Failed to connect to database.", 500, $e); - } - - return $_db; + } + + return $_db; } - + /* Returns an array of classes based on the given SQL and bound parameters (see PDO docs for details) */ - function select($sql, $bound_parameters = array()) { + function select($sql, $bound_parameters = array()) { + + // Expand any array parameters + $this->expand_select_params($sql, $bound_parameters); + $this->statement = $this->prepare($sql, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY)); $this->statement->execute($bound_parameters); $resultset = $this->statement->fetchAll(PDO::FETCH_CLASS); $this->errors(); return $resultset; } + /* Return a single row (throws on > 1 row) */ function select_row($sql, $bound_parameters = array()) { - $r = $this->select($sql, $bound_parameters); - $c = count($r); - if ($c === 0) throw new Exception("Found 0 rows", 500); - elseif ($c !== 1) throw new Exception("Too many rows (".(count($r)).") returned from '$sql'", 500); - return $r[0]; + $r = $this->select($sql, $bound_parameters); + $c = count($r); + if ($c === 0) throw new Exception("Found 0 rows", 500); + elseif ($c !== 1) throw new Exception("Too many rows (".(count($r)).") returned from '$sql'", 500); + return $r[0]; } - + + /* Return a simple type and object mapped set of records + + Uses column aliases and type designation to generate object hierarchy. + + Features: + + * simple key format `any.number.of.subkeys:optional_type` + * order of columns is not important + * allows values to be type cast + + Not supported: + + * combining rows into sub-objects + + Example: + + SELECT + SomeID AS `id:int`, + FirstName AS `name.first`, + LastName AS `name.last`, + AnotherColumn AS `other` + FROM SomeTable + + + [{ + id: 1234, + name: { + 'first': "Sideshow", + 'last': "Bob" + }, + other: "some value" + }, ...] + */ + function select_objects($sql, $bound_parameters = array()) { + + $rows = $this->select($sql, $bound_parameters); + + $d = '.'; $t = ':'; // delimiters for depth and type + $o = array(); // output + + foreach ($rows as $r) { // each row in result set + + $row = array(); + $type = ''; + + foreach ($r as $key => $value) { // each column + + if (strpos($key, $t)) { + // extract type + $p = explode($t, $key); + $type = $p[1]; + $key = $p[0]; + } else { + $type = null; // no type provided + } + + // extract keys + $keys = strpos($key, $d) ? explode($d, $key) : array($key); + + $ptr = &$row; // working pointer + + // create objects as needed + foreach ($keys as $k) { + if (!isset($ptr[$k])) $ptr[$k] = array(); + $ptr = &$ptr[$k]; + } + + // adjust type + if (!empty($type)) settype($value, $type); + + // add column + if (empty($ptr)) $ptr = $value; + else $ptr[] = $value; + } + + $o[] = db::array_to_object($row); // add row to output as objects + } + + return $o; + } + /* Provide a wrapper for updates which is really just an alias for the query function. */ - function update($sql, &$bound_parameters = array()) { + function update($sql, $bound_parameters = array()) { $this->query($sql, $bound_parameters); - if ($this->statement->rowCount() === 0) - throw new Exception('Update failed: resource was not found.', 404); + return $this->statement->rowCount() > 0; // optional success condition, leave to caller to decide } - /* + /* General query. Use this when no specific results are expected. Example: attempting to delete a resource that may or may exist. - - $bound_parameters is an array of arrays with the 'value' member as the value to bind and the 'pdoType' as + + $bound_parameters is an array of arrays with the 'value' member as the value to bind and the 'pdoType' as that parameter's PDO datatype. */ - function query($sql, &$bound_parameters = array()) { + function query($sql, $bound_parameters = array()) { + // Check if the params have been bound or not already + if (!$this->is_bound($bound_parameters)) + $bound_parameters = $this->bind_parameters($bound_parameters); + + // Expand any array parameters + $this->expand_query_params($sql, $bound_parameters); + $this->statement = $this->prepare($sql); - if (!empty($bound_parameters)) { + if (!empty($bound_parameters)) { foreach ($bound_parameters as $key => &$param) { $v = $param['value']; $t = $param['pdoType']; if (!$this->statement->bindValue($key, $v, $t)) throw new Exception("Unable to bind '$v' to named parameter ':$key'.", 500); } } - - $this->statement->execute(); - $this->errors(); + + try { + $this->statement->execute(); + $this->errors(); + } catch (Exception $e) { + $this->errors(); // force errors() to run even after exceptions (for PDO::ERRMODE_EXCEPTION) + } + } - + /* Provide a wrapper for inserts which is really just an alias for the query function. */ - function insert($sql, &$bound_parameters = array()) { + function insert($sql, $bound_parameters = array()) { $this->query($sql, $bound_parameters); if ($this->statement->rowCount() === 0) - throw new Exception('Insert failed: no rows were inserted.', 500); + throw new Exception('Insert failed: no rows were inserted.', 409); } - + + /* + Manage a multiple insertion + + * `$sql`: an `INSERT` statement of the form `INSERT INTO Foo (C1, ..., Cn) VALUES (:key1, ..., :keyn)` + * `$dataTypes`: an array containing the PDO data types of the data values of the form + `array(0 => PDO::dataType, ..., n => PDO::dataType)` + * `$data`: a 2D array of the form `array(0 => array(key1 => v01, ..., keyn => v0n), ..., m => array(key1 => vm1, ..., keyn => vmn))` + + 1. Handled with `m` `INSERT` statements wrapped in a transaction. + 2. The `INSERT` is prepared first (via PDO). + 3. `m` `INSERT` statements are then executed, binding parameters at execution time. + */ + public function multi_insert($sql, $dataTypes, $data) { + + if (empty($data)) + throw new Exception('Aborting: no data to insert.', 500); + + if (empty($dataTypes)) + throw new Exception('Aborting: no data-types provided for data values (necessary for binding).', 500); + + try { + $this->beginTransaction(); + + $this->statement = $this->prepare($sql); + + foreach ($data as $i => $row) { + if (count($row) !== count($dataTypes)) + throw new Exception("Transaction rolled back as parameter count does not match data value count for data row $i", 500); + + $index = 0; + foreach ($row as $key => $v) { + $t = $dataTypes[$index]; + if (!$this->statement->bindValue($key, $v, $t)) + throw new Exception("Unable to bind '$v' to named parameter ':$key'.", 500); + $index++; + } + $this->statement->execute(); + $this->errors(); + } + + $this->commit(); + } + catch (Exception $e) { + $this->rollBack(); + $m = $e->getMessage(); + throw new Exception("Transaction failed and rolled back: $m", 500); + } + } + /* Provide a wrapper for deletes which is really just an alias for the query function. */ - function delete($sql, &$bound_parameters = array()) { + function delete($sql, $bound_parameters = array()) { $this->query($sql, $bound_parameters); if ($this->statement->rowCount() === 0) throw new Exception('Delete failed: resource does not exist.', 404); } - + /* Return the number of rows affected by the last INSERT, DELETE, or UPDATE. */ function affected_rows() { return $this->statement->rowCount(); } - + + /* + Generates a PDO bound parameterized array. + + Pass this an array of keys and values that you want to use in your DB query. + generate_params() will return you a valid PDO array to use with + your INSERT and UPDATE statements. + + View the test harness for this here: https://gist.github.com/ngallagher87/6717925 + + Supported types: + ================= + PARAM_BOOL + PARAM_NULL + PARAM_INT + PARAM_STR + + Unsupported types: + ================= + PARAM_LOB + PARAM_INPUT_OUTPUT + PARAM_STMT (No drivers support this anyways) + + Note: + + If you need to use one of these unsupported types, you'll have to + generate the params by hand. + + Example: + ======== + + $sql = << 'tuesday', + 'dayNumber' => 2, + 'isHoliday' => true + ); + $this->db->insert($sql, $params); + */ + public function bind_parameters($array) { + $find_type = function($val) { + $pdoType = PDO::PARAM_NULL; + if (is_numeric($val)) $pdoType = PDO::PARAM_INT; + else if (is_bool($val)) $pdoType = PDO::PARAM_BOOL; + else if (is_string($val)) $pdoType = PDO::PARAM_STR; + return $pdoType; + }; + + $params = array(); + foreach ($array as $key => $val) { + $pdoType = PDO::PARAM_NULL; + if (is_array($val)) { + $type = $find_type(current($val)); + foreach ($val as $k => $v) { + if ($find_type($v) !== $type) + throw new Exception('Array contents must have the same type. '. + 'Cannot bind parameters', 400); + } + $pdoType = $type; + } else { + $pdoType = $find_type($val); + } + $params[$key] = array('value' => $val, 'pdoType' => $pdoType); + } + return $params; + } + + /* + Tests to see if parameters have been bound or not. + Returns true if they are bound, and false if they are not. + + An array is either bound or not - there are no partial cases. + */ + private function is_bound($array) { + foreach ($array as $key => $val) { + if (is_array($val)) { + if (!isset($val['pdoType']) || + !in_array($val['pdoType'], self::$valid_pdo_types)) + return false; + } + else return false; + } + return true; + } + /* Throw error info pertaining to the last operation. */ private function errors() { $e = $this->statement->errorInfo(); @@ -105,6 +344,125 @@ private function errors() { else if (!empty($e[0]) && !empty($e[2])) { throw new Exception($e[2], 500); } + else if (!empty($e[0]) && strcmp($e[0], 'HY093') === 0) { + throw new Exception('Error HY093: Check your PDO field bindings', 500); + } else throw new Exception('Update failed for unknown reason.', 500); } -} \ No newline at end of file + + /* + Utility: expand any parameters passed in as an array (as PDO does not yet support this). + + For any parameter `p => array( k1 => v1, k2 => v2, ..., kn => vn)` in `$params`, + `p` is expanded to `p_k1 => v1, p_k2 => v2, ..., p_kn => vn` all members of `$params`. + + For any label `:p` in `$sql`, `p` is replaced with `:p_k1, :p_k2, ..., :p_kn` in `$sql`. + */ + private function expand_select_params(&$sql, &$params) { + + $expanded = array(); // store expanded parameters until we're done + $expandedKeys = array(); // store the keys of expanded arrays so we can unset them when we're done + + foreach ($params as $p => $arrayParam) { + + // only worry about arrays + if (!is_array($arrayParam)) continue; + + // Empty arrays need to be handled explicitly so they don't cause array to string conversion exceptions at bind time. + if (empty($arrayParam)) { + $params[$p] = ''; + continue; + } + + $expandedKeys[] = $p; + $names = ''; // list of labels `:p_k1, :p_k2, ..., :p_kn` with which to replace `:p` in $sql + foreach ($arrayParam as $k => $v) { + + // Ensure validity of members + if ((!empty($v) && !is_scalar($v))) { + $t = gettype($v); + throw new Exception("Array parameter expansion error: members of array '$p' must be scalars, but '$k' is a '$t'.", 500); + } + + $expanded["{$p}_{$k}"] = $v; + $names .= ":{$p}_{$k}, "; + } + + // replace :p in $sql with $names + $names = rtrim($names, " ,"); + $sql = str_replace(":$p", $names, $sql); + } + + // Nothing expanded + if (empty($expandedKeys)) return; + + // Get rid of the now expanded array params + foreach ($expandedKeys as $v) + unset($params[$v]); + + // Merge in the expanded values + $params = array_merge($params, $expanded); + } + + /* + Utility: expand any parameters passed in as an array (as PDO does not yet support this). + + For any parameter `p => array('value' => array( k1 => v1, k2 => v2, ..., kn => vn), 'pdoType' => PDO::SOME_PDO_TYPE)` in `$params`, + `p` is expanded to + `p_k1 => array('value' => v1, 'pdoType' => PDO::SOME_PDO_TYPE), + p_k2 => array('value' => v2, 'pdoType' => PDO::SOME_PDO_TYPE), + ..., + p_kn => array('value' => vn, 'pdoType' => PDO::SOME_PDO_TYPE)` + all members of `$params`. + + For any label `:p` in `$sql`, `p` is replaced with `:p_k1, :p_k2, ..., :p_kn` in `$sql`. + */ + private function expand_query_params(&$sql, &$params) { + + $expanded = array(); // store expanded parameters until we're done + $expandedKeys = array(); // store the keys of expanded arrays so we can unset them when we're done + + foreach ($params as $p => $arrayParam) { + + // only worry about arrays + if (!is_array($arrayParam['value'])) continue; + + // Empty arrays need to be handled explicitly so they don't cause array to string conversion exceptions at bind time. + if (empty($arrayParam['value'])) { + $params[$p] = array('value' => '', 'pdoType' => PDO::PARAM_STR); + continue; + } + + $expandedKeys[] = $p; + $names = ''; // list of labels `:p_k1, :p_k2, ..., :p_kn` with which to replace `:p` in $sql + foreach ($arrayParam['value'] as $k => $v) { + + // Ensure validity of members + if ((!empty($v) && !is_scalar($v))) { + $t = gettype($v); + throw new Exception("Array parameter expansion error: members of array '$p' must be scalars, but '$k' is a '$t'.", 500); + } + + $expanded["{$p}_{$k}"] = array('value' => $v, 'pdoType' => $arrayParam['pdoType']); + $names .= ":{$p}_{$k}, "; + } + + // replace :p in $sql with $names + $names = rtrim($names, " ,"); + $sql = str_replace(":$p", $names, $sql); + } + + // Nothing expanded + if (empty($expandedKeys)) return; + + // Get rid of the now expanded array params + foreach ($expandedKeys as $v) + unset($params[$v]); + + // Merge in the expanded values + $params = array_merge($params, $expanded); + } + + // convert an array to an object (recursively) + public static function array_to_object($o) { return is_array($o) ? (object) array_map(__METHOD__, $o) : $o; } +} diff --git a/lib/delegator-index.php b/lib/delegator-index.php index e1eaacc..815b44d 100644 --- a/lib/delegator-index.php +++ b/lib/delegator-index.php @@ -1,16 +1,24 @@ getCode(); $message = $e->getMessage(); $via = $e->getPrevious(); - + if (PRESTO_DEBUG) { $detail = (is_object($p)) ? $p::call : $e->getTrace(); $payload = array('message' => $message , 'code' => $n, 'detail' => $detail); @@ -20,7 +28,9 @@ if ($via) $payload['error'] = array('message' => $via->getMessage(), 'code' => $via->getCode()); - error_log(json_encode($payload)); // also send to syslop + if (PRESTO_TRACE) $payload[PRESTO_TRACE_KEY] = Presto::trace_info(); + + error_log(json_encode($payload)); // also send to syslog header("HTTP/1.0 $n API error"); header("Content-Type: application/json"); diff --git a/lib/encoders/html.php b/lib/encoders/html.php new file mode 100644 index 0000000..c7622f3 --- /dev/null +++ b/lib/encoders/html.php @@ -0,0 +1,41 @@ + array('body' => $node))); + } + + $indent = str_repeat("\t", $d); // indent for pretty printing + + if (is_string($node)) return print "\n$indent$node"; + elseif (is_array($node)) { + + // descend into child nodes + + $d++; + foreach ($node as $k => &$v) { + $a = ''; + + if (empty($k) || is_numeric($k)) + $k = 'li'; // assume lists are LIs + + if (is_callable($mapper)) + $k = $mapper($k, $v, $a, $d); + + // print node + + print "\n$indent<$k$a>"; + _encode_html($v); // recurse + print "\n$indent"; + } + $d--; + } +} + diff --git a/lib/extras/tagged-sql.php b/lib/extras/tagged-sql.php deleted file mode 100644 index 4929eb2..0000000 --- a/lib/extras/tagged-sql.php +++ /dev/null @@ -1,219 +0,0 @@ - Array - ( - [@attributeName] => attributeValue - ) - ~~~ - - Simple node tags add a simple node to the current (complex) node of the - DOM. Simple nodes will have a value and may have a label or classes, but that's it, - they do not contain complex nodes or other attributes. They are leaves in the DOM: - ~~~ - [simpleNode] => Array - ( - [@label] => Human readable label, - [@class] => class1 class2 class3 ... classn - [value] => The value pulled from the DB - ) - ~~~ - - ## Result set row ordering - - The transform depends on the row ordering of the result set. The purpose of this - transform is to take flat result sets and transform them into the tree they represent. - - For example the result set: - ~~~ - | RubricID | DomainID | ElementID | - | -------- | -------- | --------- | - | 1 | 1 | 1 | - | 1 | 1 | 2 | - | 1 | 2 | 3 | - | 1 | 2 | 4 | - ~~~ - - Is transfomed into the DOM: - ~~~ - - |¯¯¯¯¯¯¯¯| - | Rubric | - | @id:1 | - |________| - | - | |¯¯¯¯¯¯¯¯| - |------->| Domain |------------->|¯¯¯¯¯¯¯¯¯| - | | @id:1 | | | Element | - | |________| | | @id:1 | - | | |_________| - | | - | |----->|¯¯¯¯¯¯¯¯¯| - | | Element | - | | @id:2 | - | |_________| - | - | |¯¯¯¯¯¯¯¯| - |------->| Domain |------------->|¯¯¯¯¯¯¯¯¯| - | @id:2 | | | Element | - |________| | | @id:3 | - | |_________| - | - |----->|¯¯¯¯¯¯¯¯¯| - | Element | - | @id:4 | - |_________| - - - - ~~~ - - Note that if you do not order the rows in your result set in this way then the - there is no guarantee about the shape of the resulting DOM, except that it will - be wrong. - - The $stripMeta flag determines whether attributes will begin with the '@' symbol. - You may want this symbol stripped off if, for instance, you will be transmitting - the result of this call as JSON data to a client. This flag has no effect on the - special '@class' and '@label' reserved keys. - - */ - public function &dominate(&$rs, $asObj = true, $stripMeta = false) { - $dom = array(); - if (empty($rs)) return $dom; - - $r_constructorNode = '/^#(\w+)(?:\(.*\))?(?:\[(.+)\])?$/'; - $r_simpleNode = '/^(@?\w+)(?:\(.*\))?(?:\[(.+)\])?$/'; - $r_classes = '/^.*\((.*)\).*$/'; - - foreach ($rs as $row) { - $currentNode = &$dom; - foreach ($row as $key => $value) { - - if (!isset($value)) $value = ''; // clamp null values to ''; - - // get the node level classes (if any) - $classes = null; - if (preg_match($r_classes, $key, $matches)) { - $classes = $matches[1]; - } - - if (preg_match($r_constructorNode, $key, $matches)) { - // this tag will construct a node. - if ($value === '') { - // no value means no ID ... we can't do anything with this - break; - } - - $nodeName = $matches[1]; - - // move into the node - $currentNode = &$currentNode[$nodeName][$value]; - - // set the node level class(es) - if (isset($classes)) { - $currentNode['@class'] = $classes; - } - - // set the node label if there is one - if (isset($matches[2]) && trim($matches[2]) != '') { - $currentNode['@label'] = trim($matches[2]); - } - } - else if (preg_match($r_simpleNode, $key, $matches)) { - // this tag will construct a simple node (or attribute). - $nodeName = $matches[1]; - - // create the node (or attribute) - if (strpos($nodeName, '@') === 0) { - // create the attribute - $nodeName = $stripMeta ? substr($nodeName, 1) : $nodeName; - $currentNode[$nodeName] = $value; - } - else { - // create the simple node - $currentNode[$nodeName] = array(); - - // set the node label if there is one - if (isset($matches[2]) && trim($matches[2]) != '') { - $currentNode[$nodeName]['@label'] = trim($matches[2]); - } - - // set the node level class(es) - if (isset($classes)) { - $currentNode[$nodeName]['@class'] = $classes; - } - - // set the node value - $currentNode[$nodeName]['value'] = $value; - } - } - } - } - if (empty($dom)) return $dom; - if ($asObj) return (object) $dom; - return $dom; - } -} \ No newline at end of file diff --git a/lib/htaccess-example b/lib/htaccess-example index 68e6ebd..ce50046 100644 --- a/lib/htaccess-example +++ b/lib/htaccess-example @@ -13,7 +13,7 @@ RewriteRule lib/(.*) /404.html [R,L] # Route API requests RewriteCond %{REQUEST_FILENAME} !-f -RewriteRule (.*)\.(.*)$ /delegator.php?r=$1&t=$2& [QSA,L] +RewriteRule (.*)\.(.*)$ /delegator.php?r=$1&t=$2 [QSA,L] RewriteRule ^(.*)\.html$ $1.php [NC,L] # General requests (enable for standard php/index delegate behaviour) diff --git a/lib/inc.php b/lib/inc.php index 7470f42..2656392 100644 --- a/lib/inc.php +++ b/lib/inc.php @@ -1,3 +1,3 @@ - - - -Presto install checklist - - - - - -
    -

    Presto install checklist

    -
    - - -
    - -
    - Presto debug mode -

    Debug mode adds trace and informational logging via syslog and extra detail in exceptions.

    -
    - -
    -= '5' && $ver[1] >= 3); -?> - PHP version ok? -

    Found PHP version

    -
    - -
    - - cURL/PHP installed? -

    cURL details:

    -
    - -
    - - JSON/PHP installed? -

    Determines if JSON support is enabled.

    -
    - - -
    - - HTACCESS enabled? -

    Validates that both .htaccess rules and non-API delegation are functional.

    -
    - -
    - Presto delegation ok? -

    Validates basic Presto delegation. This test looks for a valid Presto 404.

    - - -
    - -
    - Presto delegate execution? -

    Validates sample Presto delegation.

    - - -
    - -
    - Presto header magic? -

    Validates Presto header functions.

    - - -
    - - -
    - -Request details -
    -
    -
    -
    -
    -
    -
    - -
    - - - diff --git a/lib/presto.php b/lib/presto.php index 6d0a2ce..561b39e 100644 --- a/lib/presto.php +++ b/lib/presto.php @@ -1,42 +1,16 @@ _base = $_SERVER['DOCUMENT_ROOT']; @@ -45,123 +19,166 @@ public function __construct() { self::$req = new request(); try { - $this->filter(); // request filtering - $this->dispatch(); // dispatch to loaded class->member based on $req + $this->dispatch(); // dispatch to loaded class->member based on $req } catch (Exception $e) { throw $e; // errors are handled by delegator } } - - /** Apply request filters */ - private function filter() { - } - - private static function autoload_explicit($class) { - // First look in the base directory for the web app - $class_file = strtolower($class) . ".php"; - if (file_exists($class_file)) - return require_once($class_file); - - throw new Exception("API `$class` not found.", 404); - } - + /* Dispatch requests to classes and class methods */ private function dispatch() { - + try { - - $obj = self::$req->uri->component('error' /* default to an error route */); - $action = self::$req->action; // determines the request action (method) - $thing = self::$req->uri->thing(); // determine the thing (resource) - - // Create an an instance of the API subclass - // (autoloads based on the path) - self::autoload_explicit($obj); - if (!class_exists($obj)) throw new Exception("API not found for $obj", 404); - $o = new $obj(); - - // validate that the concept noun is valid - if (!$o->is_valid_concept($thing)) - $thing = ''; // no thing (resource) available, assume root action - - // build the call pseudo object - $method = (strlen($thing)) ? "{$action}_{$thing}" : $action; - $this->call = (object) array( - 'class' => $obj, - 'method' => $method, - 'res' => self::$req->uri->type(), - 'params' => self::$req->uri->parameters, - 'options' => self::$req->uri->options, - 'exists' => false); - - // build the response object + + $this->call = self::$req->scheme(); + + $action = self::$req->action; // the request action (method) + $obj = $this->call->class; + $method = $this->call->method; + $preflight = $this->call->preflight; + $type = self::$req->type; + $model = null; + + $res = $this->call->resource; // the root resource + + presto_lib::_trace('REQUEST', "[{$this->call->file}] $obj::$method ({$this->call->type})", + json_encode($this->call->params), json_encode($this->call->options)); + + // Create an an instance of the API subclass (autoloaded) + + autoload_delegate($this->call); + + if (!class_exists($obj)) + throw new Exception("API class not found for $obj::$method", 404); + + // Start the response setup + self::$resp = new response($this->call); - - // verify the request - + + API::attach( $this->call, self::$resp, self::$req ); + + $o = new $obj(); + + // Verify the request + if ($obj == 'error') // disallow root component access throw new Exception('Root access not allowed', 403); - - if (!method_exists($obj, $method)) // check that the resource is valid - throw new Exception("Can't find $obj->$method()", 404); - - $this->call->exists = true; - - self::_trace("Dispatching to $obj :: $method"); - - // delegate - $o->attach( $this->call, self::$resp, self::$req ); - $this->call->data = $o->$method( $this->call, self::$req->body() ); - - // return the result to the client - $encode = (is_object($this->call->data) || is_array($this->call->data)); + if (!method_exists($obj, $preflight)) { + + if (method_exists($obj, 'autoload_model')) { + + // try a default model autoloader "preflight" + $model = $o->autoload_model( + $this->call->params, + $this->call->options, + self::$req->body(), + $this->call->type); + + } else { + + // skip + trace missing preflight functions (data will be passed as standard HTTP params) + + presto_lib::_trace('PREFLIGHT', 'skipped', + "[{$this->call->file}] $obj::$preflight ({$this->call->type})", + json_encode($this->call->params), json_encode($this->call->options)); + } + + } else { + + // attempt a custom "preflight" model autoload call + + $model = $o->$preflight( + $this->call->params, + $this->call->options, + self::$req->body(), + $this->call->type ); + } + + if (!method_exists($obj, $method)) // valid route? + throw new Exception("Resource $obj->$method not found.", 404); + + $this->call->exists = true; + + // Perform the actual sub delegation + + if (isset($model)) + $this->call->data = $o->$method( $model, $this->call->type ); + else + $this->call->data = $o->$method( + $this->call->params, + $this->call->options, + self::$req->body(), + $this->call->type ); + + // Produce a response for the client + + presto_lib::_trace( PRESTO_TRACE_KEY, json_encode(Presto::trace_info()) ); + $profiles = Profiler::profiles(); // add any process profiling to trace + if (!empty($profiles)) + presto_lib::_trace( PROFILER_TRACE_KEY, json_encode($profiles) ); + + $encode = (is_object($this->call->data) || is_array($this->call->data)); return self::$resp->ok( $this->call, $encode, $o->status(), $o->headers() ); - + } catch (Exception $e) { - if (self::$resp === null) - self::$resp = new response(); - + if (self::$resp === null) self::$resp = new response(); + self::$resp->hdr($e->getCode()); throw $e; } } - /** Presto failures (including many PHP failures) */ + /* Handle PrestoPHP failures (including all catchable PHP failures) */ static public function fail($n, $text, $file, $line, $ctx) { // set up pseudo call and response if (self::$resp === null) self::$resp = new response($ctx); - + // generate useful HTTP status switch ($n) { case 2: $status = 400; break; default: $status = 500; } - // build the resulting error object - $details = (object) array( + $details = array( 'status' => $status, 'code' => $n, - 'error' => $text, + 'error' => $text, 'file' => $file, 'line' => $line, 'ctx' => $ctx ); + if (PRESTO_TRACE) $details[PRESTO_TRACE_KEY] = Presto::trace_info(); + + // build the resulting error object + $details = json_encode( (object) $details); + + error_log('FATAL: ' . json_encode(array($status, $details))); self::$resp->hdr($status); - print json_encode($details); + print $details; die; } + // Get trace info for a call + static public function trace_info() { + return array( + 'routing_scheme' => self::$req->scheme(), + 'body' => self::$req->body(), + 'request' => self::$req->uri, + 'version' => PRESTO_VERSION + ); + + } /** Debugging dump of Presto delegator */ public function __toString() { return print_r($this, true); } } /** REST base class - Handy constants and base methods + Constants and base methods */ class REST { @@ -169,14 +186,8 @@ class REST { 'get', 'put', 'post', 'delete', 'options', 'head'); public static $TYPES = array( 'json', 'xml'); - + public static $req; - public static $resp; - public static $sess; - - public static function _trace() { - if (PRESTO_DEBUG == 0) return; - error_log("TRACE: ".implode("\n\t", func_get_args())); - } - + public static $resp; + public static $sess; } diff --git a/lib/profiler.php b/lib/profiler.php new file mode 100644 index 0000000..f877ecb --- /dev/null +++ b/lib/profiler.php @@ -0,0 +1,127 @@ + 0, + 'duration_units' => 'microseconds', + 'last_started' => microtime(true) * 1000000, + 'state' => 'running', + 'extra' => $extra + ); + } + else + presto_lib::_trace( __METHOD__, "[$key]: is already being tracked."); + } + + /* + Turn timer off for process identified by $key and calculate duration since the last time it was turned on. + */ + public static function stop($key) { + + if (empty(self::$processes[$key])) { + presto_lib::_trace( __METHOD__, "Failed to stop [$key]: it isn't being tracked."); + return; + } + + $p = &self::$processes[$key]; + + if ($p->state === 'running') { + // toggle off + $p->duration += microtime(true) * 1000000 - $p->last_started; + $p->state = 'stopped'; + } + } + + /* + Restart timer for process identified by $key. + + * Optional $extra debugging data is merged with any set in Profiler::track + */ + public static function restart($key, $extra = null) { + + if (empty(self::$processes[$key])) { + presto_lib::_trace( __METHOD__, "Failed to restart [$key]: it isn't being tracked."); + return; + } + + if (isset($extra) && !is_array($extra)) { + presto_lib::_trace( __METHOD__, "Failed to restart [$key]: optional extra data must be an array."); + return; + } + + $p = &self::$processes[$key]; + + if ($p->state === 'stopped') { + // toggle on + $p->last_started = microtime(true) * 1000000; + $p->state = 'running'; + } + + if (is_array($extra)) + $p['extra'] = array_merge($p['extra'], $extra); + } + + /* + Return the profile of the process identified by $key. + */ + public static function profile($key) { + + if (empty(self::$processes[$key])) return; + + $p = &self::$processes[$key]; + + if ($p->state === 'running') { + // calc current durations for output + $p->duration += microtime(true) * 1000000 - $p->last_started; + } + + return $p; + } + + /* + Return all profile data. + */ + public static function profiles() { + + if (empty(self::$processes)) return; + + foreach (self::$processes as &$p) { + if ($p->state === 'running') { + // calc current durations for output + $p->duration += microtime(true) * 1000000 - $p->last_started; + } + } + + return self::$processes; + } +} \ No newline at end of file diff --git a/lib/request.php b/lib/request.php index fcac9e0..714c736 100644 --- a/lib/request.php +++ b/lib/request.php @@ -1,205 +1,164 @@ raw = $uri; - $uri = (object) parse_url(ltrim($uri, '/')); - - if (empty($uri->path)) $uri->path = ''; - - $this->type = pathinfo($uri->path, PATHINFO_EXTENSION); - - $uri->path = str_replace('.'.$this->type, '', $uri->path); - $this->path = str_replace($this->type, '', $uri->path); - - $this->parameters = explode('/', $this->path); +class Request { - $this->options = $_GET; - - $this->payloadType = $this->content_type($this->type); - } - - // get the resource type - public function type() { return $this->type; } - // is the resource a folder? - public function isFolder() { return empty($this->path) || substr($this->path, -1) === '/'; } - // get the resource full path - public function path() { return empty($this->path) ? '/' : $this->path; } - // get the resource type of the request payload - public function payloadType() { return $this->payloadType; } - // get the resource extension - public function ext() { return !empty($this->type) ? '.'.$this->type : ''; } - // get the resource name - public function res() { return implode('/', $this->parameters) . $this->ext(); } - // get a URI flag (returns true/false) - public function flag($f) { return $this->opt($f) !== NULL; } - // get a URI option (a GET parameter) - public function opt($k) { return (array_key_exists($k, $this->options)) - ? $this->options[$k] : NULL ; } - // get all of the URI optins as an object - public function options() { return (object) $this->options; } - // get the thing that this URI refers to - public function thing() { return !empty($this->parameters[1]) ? str_replace('-', '_', $this->parameters[1]): ''; } - // get the component that this URI refers to - public function component($d) { - return presto_lib::coalesce( - str_replace('-', '_', reset($this->parameters)), $d ); - } - // bump a parameter off this URI - public function bump() { return array_pop($this->parameters); } - - /* - Helper: determine the content type of the call using $_SERVER['CONTENT_TYPE'] if possible. - If not default to the extension initially parsed off of the request URI. - */ - private function content_type($ext) { - - if (empty($_SERVER['CONTENT_TYPE'])) return $ext; - - $ct = strtolower($_SERVER['CONTENT_TYPE']); - $ct = explode(';', $ct); - - foreach ($ct as $v) { - $candidate = trim($v); - - switch ($candidate) { - case 'text/html': - return 'html'; - - case 'application/json': - return 'json'; - - default: - return $ext; - } - } - } - -} + public $uri; // source URI + public $path; // path of source URI (minus any query and fragment) -/** A RESTful request + public $container; // target API container + public $route; // target API route + public $type; // resource type (based on URI) -Decodes and makes available various portions of a request, including: + public $host; // hostname + public $tld; // top level domain name + public $scheme; // request scheme (http, https, etc.) + public $method; // requested method + public $action; // presto's target action + public $service; // service name + public $query; // query parameters + public $get; // get parameters + public $post; // post parameters + public $options; // query options + public $referer; // the likely referring URI + public $refererPath; // path of the likely referring URI (referer minus any query and fragment) -* URI -* POST -* encoded bodies + /* Set up a request object (from PHP builtins) */ + public function __construct($r = null, $t = null, $c = null) { -*/ -class Request { + $this->uri = $_SERVER['REQUEST_URI']; + $this->path = parse_url($this->uri, PHP_URL_PATH); + + // set up basic delegation concepts (via params or htaccess) + + $this->container = presto_lib::_get('c', $c); + $this->route = presto_lib::_get('r', $r); + $this->type = presto_lib::_c(presto_lib::_get('t', $t), 'json'); + $params = $this->params(); + + if (!array_key_exists('r', $_GET) || !array_key_exists('t', $_GET) || !array_key_exists('c', $_GET)) + presto_lib::_trace("Rewrite delegation setup for {$this->uri} is missing.", json_encode($_GET)); + + unset($_GET['t']); unset($_GET['r']); unset($_GET['c']); // pop routing parameters + + // setup request parameters - public $host; - public $method; - public $action; - public $service; - public $uri; - public $query; - public $get; - public $post; - - /* Set up a request object (from PHP builtins) */ - public function __construct($r = null, $t = null) { - - // Use the URI from either .htaccess routing or the raw request - $uri = $_SERVER['REQUEST_URI']; - - // Extract route and type from delegation - if (isset($r)) $_GET['r'] = $r; if (isset($t)) $_GET['t'] = $t; // override via ctor - - if (!array_key_exists('r', $_GET) || !array_key_exists('t', $_GET)) - throw new Exception('Missing rewrite delegation setup.', 500); - - $type = array_key_exists('t', $_GET) ? $_GET['t'] : 'json'; - $uri = $_GET['r'].'.'.$type; - unset($_GET['t']); - unset($_GET['r']); - - // bootstrap request parameters - $this->uri = new URI($uri); $this->method = strtolower($_SERVER['REQUEST_METHOD']); - $this->action = presto_lib::coalesce($this->method, 'get'); + $this->action = presto_lib::_c($this->method, 'get'); // default to GET $this->host = $_SERVER['HTTP_HOST']; + + $this->referer = _server('HTTP_REFERER', ''); + $this->refererPath = parse_url($this->referer, PHP_URL_PATH); + $this->service = strstr($this->host, '.', -1); + $this->tld = pathinfo($this->host, PATHINFO_EXTENSION); + $this->scheme = _server_has('HTTPS', 'on') + || _server_has('HTTP_X_FORWARDED_PROTO', 'https') + || _server_has('HTTP_X_FORWARDED_SSL', 'on') ? 'https' : 'http'; + + $this->options = $_GET; + $_GET = array(); // discourage use of $_GET + } + + /* Get the request mapping scheme */ + public function scheme() { + $p = explode('/', presto_lib::_cleanup($this->route)); + $class = presto_lib::_at($p, 0, ''); + $res = presto_lib::_at($p, 1, ''); + $file = empty($this->container) ? "$class.php" : "$this->container/$class.php"; + $method = empty($res) ? $this->method : $this->method . '_' . $res; + $preflight = "{$method}_model"; + + return (object) array( + 'container' => presto_lib::_cleanup($this->container), + 'class' => presto_lib::_cleanup($class), + 'file' => str_replace('/.', '.', $file), + 'resource' => $res, + 'type' => $this->type, + 'action' => $this->method, + 'method' => presto_lib::_cleanup($method), + 'preflight' => presto_lib::_cleanup($preflight), + 'params' => $this->params(), + 'options' => $this->options, + 'referer' => $this->referer + ); + } + public function params() { + $p = explode('/', presto_lib::_cleanup($this->route)); + $p = array_slice($p, 2, count($p)); + + return $p; + } - // reset wrapped globals - $_GET = array(); - - + /* Determine if this is an internal request + Not intended for secure uses (informational only), as the referer field can be spoofed. + */ + public function isInternalRequest() { + $via = parse_url($this->referer, PHP_URL_HOST); + return ($this->host === $via); } - - /** Get a GET value (or values) - + /* Get a GET value (or values) + Relies on PHP's built in filtering mechanics. These are a reliable, thourough set of filters. Learn them. Use them. - + $f : Either the parameter to get, or the set of parameters and filters (based on the filter_input* APIs) - - Returns the value or values requested. Caches values for debugging and other - + + Returns the value or values requested. Caches values for debugging and other + See parameter definitions for: - + http://php.net/manual/en/function.filter-input-array.php http://www.php.net/manual/en/function.filter-input.php */ - public function get($f = null) { - + public function get($f = null, $throw = true) { + if ( $this->get = $this->filter(INPUT_GET, $f) ) - return (object) $this->get; - - throw new Exception('Missing or invalid GET parameter', 400); + return (object) $this->get; + + if ($throw) + throw new Exception('Missing or invalid GET parameter', 400); } - - /** Get a post value (or values) - + + /* Get a post value (or values) + Relies on PHP's built in filtering mechanics. These are a reliable, thourough set of filters. Learn them. Use them. - + $f : Either the parameter to get, or the set of parameters and filters (based on the filter_input* APIs) - - Returns the value or values requested. Caches values for debugging and other - + + Returns the value or values requested. Caches values for debugging and other + See parameter definitions for: - + http://php.net/manual/en/function.filter-input-array.php http://www.php.net/manual/en/function.filter-input.php */ - public function post($f = null) { - + public function post($f = null, $throw = true) { + if ( $this->post = $this->filter(INPUT_POST, $f) ) - return (object) $this->post; - - throw new Exception('Missing or invalid POST parameter', 400); + return (object) $this->post; + + if ($throw) + throw new Exception('Missing or invalid POST parameter', 400); } - + private function filter($type, $f = null) { - + $r = null; $in = $type === INPUT_POST ? $_POST : $_GET; - + if (is_array($f)) { $r = filter_input_array($type, $f); } elseif (is_string($f)) { @@ -209,68 +168,110 @@ private function filter($type, $f = null) { foreach ($in as $k => $v) $r($k); } - + if ( $r ) - return (object) $r; - + return (object) $r; + return false; } - /** Get a request body - - Currently handles content types 'application/json', - 'application/xml' or 'text/xml', and 'application/x-www-form-urlencoded'. - Assumes JSON if no content type is specified. - JSON body is returned as the result of a call to json_decode. - XML body is returned as a SimpleXMLElement object. - application/x-www-form-urlencoded is returned as a string - Any other content type throws a 400 exception. - + /* Get a request body + + Currently supports: + + * 'application/json' + * 'application/xml' or 'text/xml' + * 'application/x-www-form-urlencoded' + + Assumes JSON if no content type is specified. + + Notes: + + * A JSON body is returned as the result of a call to json_decode. + * An XML body is returned as a SimpleXMLElement object. + * An application/x-www-form-urlencoded is returned as a string + * Any other content type throws a 400 exception. + */ public function body() { - $decoded_body = false; + $decoded_body = false; if ( ($body = @file_get_contents('php://input')) ) { - + if (empty($body)) return $decoded_body; // no data, not an error - switch ($this->uri->payloadType()) { + switch ($this->content_type($this->type)) { case 'json': if ( ! ($decoded_body = json_decode($body)) ) { - + $errors = array( JSON_ERROR_NONE => 'No errors.', - JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', + JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', JSON_ERROR_STATE_MISMATCH => 'Underflow or the modes mismatch', - JSON_ERROR_CTRL_CHAR => 'Unexpected control character found', + JSON_ERROR_CTRL_CHAR => 'Unexpected control character found', JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON', JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded' ); - throw new Exception('Invalid JSON request payload. ' . $errors[json_last_error()], 400); + throw new Exception('Invalid JSON request payload - ' . $errors[json_last_error()] . ' - ' . $body, 400); } break; - + case 'xml': - + $decoded_body = simplexml_load_string($body); if ( $decoded_body === false ) throw new Exception("Invalid XML request payload.", 400); break; - - - default: + + + default: $decoded_body = $body; if ( $decoded_body === false ) throw new Exception("Invalid form request payload.", 400); } - - } - + + } + return $decoded_body; } - + /* Determine the content type of the call + + Usesg $_SERVER['CONTENT_TYPE'] if possible. + + If not default to the extension initially parsed off of the request URI. + */ + public function content_type($ext) { + + if (empty($_SERVER['CONTENT_TYPE'])) return $ext; + + $ct = strtolower($_SERVER['CONTENT_TYPE']); + $ct = explode(';', $ct); + + foreach ($ct as $v) { + $candidate = trim($v); + + switch ($candidate) { + case 'text/html': + return 'html'; + + case 'application/json': + return 'json'; + + default: + return $ext; + } + } + } + // dump the object to a string public function __toString() { return print_r($this, true); } } + +/* Request helpers - TODO - move these */ + +// safely get a server variable with a default +function _server($k, $d = false) { return array_key_exists($k, $_SERVER) ? $_SERVER[$k] : $d; } +// safely check to see if a server variable is a particular value +function _server_has($k, $v, $d = false) { return array_key_exists($k, $_SERVER) ? $_SERVER[$k] == $v : $d; } diff --git a/lib/response.php b/lib/response.php index 669412d..14b3cf3 100644 --- a/lib/response.php +++ b/lib/response.php @@ -1,5 +1,9 @@ 'No Content', // (NO BODY) '205' => 'Reset Content', // (NO BODY) '206' => 'Partial Content', // (ADD'L HEADERS) - + + '303' => 'See other', '304' => 'Not modified', '400' => 'Bad request', @@ -44,14 +49,47 @@ public function __construct($ctx = null) { // register default type handlers + // JSON self::add_type_handler('application/json', function ($dom) { $json = json_encode($dom); - if (json_last_error() !== JSON_ERROR_NONE) throw new Exception('JSON encoding error #' . json_last_error(), 400); + if (json_last_error() !== JSON_ERROR_NONE) throw new \Exception('JSON encoding error #' . json_last_error(), 400); print $json; } ); + // JSONP + self::add_type_handler('application/js', function ($dom, $ctx, $map) { + + if ($ctx === null || !array_key_exists('callback', $ctx->options)) + throw new \Exception('JSONP missing callback option', 400); + + $callback = $ctx->options['callback']; + + if (strlen($callback) === 0 || !preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $callback)) + throw new \Exception("Invalid JSONP callback name: $callback", 400); + + $json = json_encode($dom); + + if (json_last_error() !== JSON_ERROR_NONE) + throw new \Exception('JSON encoding error in JSONP request - #' . json_last_error(), 400); + + print "$callback($json);"; + } ); + + // Built in HTML self::add_type_handler('.*\/htm.*', function($dom) { _encode_html($dom); } ); + // very basic CSV + self::add_type_handler('text/csv', function ($dom) { + $csv = ''; + foreach ($dom as $row) + $csv .= implode( ', ', array_map( function($i) { + if (is_array($i)) return implode(', ', $i); + else return $i; + }, $row )) . "\n"; + + print $csv; + } ); + if (PRESTO_DEBUG) self::add_type_handler('text/plain', function ($dom) { print_r($dom); } ); } @@ -68,7 +106,7 @@ public function ok($ctx, $enc = true, $c = 200, $h = null) { if (!$this->hdr($c, $h)) return false; // returns if status does not allow a body - if ($enc) return self::encode($this->content_type(), $ctx->data); + if ($enc) return self::encode($this->content_type(), $ctx->data, $ctx); else return print $ctx->data; } /* Respond with a failure */ @@ -80,10 +118,10 @@ public function fail($d, $c = 500) { /* Generate an appropriate HTTP header */ public function hdr($c = '200', $h = null) { $message = array_key_exists($c, $this->codes) ? $this->codes[$c] : 'Internal error'; - + if ($this->sentHeaders) return true; else $this->sentHeaders = 1; - + $v = defined('SERVICE_VERSION') ? SERVICE_VERSION : PRESTO_VERSION; header("HTTP/1.0 {$c} {$message}"); header(VERSION_HEADER . ': ' . $v); @@ -103,26 +141,35 @@ public function hdr($c = '200', $h = null) { return true; } + /* Redirect client */ + public function redirect($t = '500.html', $o = array(), $c = '301') { + $t = preg_match('#^http(?:s|)://#', $t) ? $t : "/$t"; + + return $this->hdr($c, array_merge($o, array('Location' => $t))); + } + /** Determine the content-type */ private function content_type() { - if (!isset($this->call) || empty($this->call->res)) + if (!isset($this->call) || empty($this->call->type)) return 'text/plain'; - if (strpos($this->call->res, '/')) return $this->call->res; // already a content-type + if (isset($this->call->type) && strpos($this->call->type, '/')) + return $this->call->type; // already a content-type - // map obvious content types (should be an array?) - switch ($this->call->res) { + // map obvious content types + switch ($this->call->type) { case 'html': case 'htm': return 'text/html'; - + case 'csv': + return 'text/csv'; default: - return 'application/' . $this->call->res; + return 'application/' . $this->call->type; } } /* Encode the response using type handlers */ - private static function encode($type, $dom) { + private static function encode($type, $dom, $ctx = array()) { $h = false; // find encoder @@ -136,50 +183,46 @@ private static function encode($type, $dom) { if (!$h) throw new Exception('Unknown resource type: ' . $type, 500); - $encode = $h->enc; - $map = $h->map; - $encode($dom, $map); + $encoder_fn = $h->enc; + $encoder_fn($dom, (object) $ctx, $h->map); } - public function __toString() { return print_r($this, true); } -} - -/* Simple HTML encoder */ -function _encode_html($node, $map = null) { - static $d = 0; - static $mapper; + /* Register default type handlers */ + private function register_default_type_handlers() { - if ($mapper === null && $map !== null) $mapper = $map; + // JSON + self::add_type_handler('application/json', function ($dom) { + $json = json_encode($dom); + if (json_last_error() !== JSON_ERROR_NONE) throw new Exception('JSON encoding error #' . json_last_error(), 400); + print $json; + } ); - if (!isset($d) || $map !== null) { - $d = -1; - return _encode_html(array('html' => array('body' => $node))); - } + // JSONP + self::add_type_handler('application/js', function ($dom, $ctx, $map) { - $indent = str_repeat("\t", $d); // indent for pretty printing + if ($ctx === null || !array_key_exists('callback', $ctx->options)) + throw new Exception('JSONP missing callback option', 400); - if (is_string($node)) return print "\n$indent$node"; - elseif (is_array($node)) { + $callback = $ctx->options['callback']; - // descend into child nodes + if (strlen($callback) === 0 || !preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $callback)) + throw new Exception("Invalid JSONP callback name: $callback", 400); - $d++; - foreach ($node as $k => &$v) { - $a = ''; + $json = json_encode($dom); - if (empty($k) || is_numeric($k)) - $k = 'li'; // assume lists are LIs + if (json_last_error() !== JSON_ERROR_NONE) + throw new Exception('JSON encoding error in JSONP request - #' . json_last_error(), 400); - if (is_callable($mapper)) - $k = $mapper($k, $v, $a, $d); + print "$callback($json);"; + } ); - // print node + // Built in HTML + self::add_type_handler('.*\/htm.*', function($dom) { _encode_html($dom); } ); - print "\n$indent<$k$a>"; - _encode_html($v); // recurse - print "\n$indent"; - } - $d--; + // Build in text (debug only) + if (PRESTO_DEBUG) + self::add_type_handler('text/plain', function ($dom) { print_r($dom); } ); } -} + public function __toString() { return print_r($this, true); } +} diff --git a/lib/service.php b/lib/service.php index e4f1d94..85017a2 100644 --- a/lib/service.php +++ b/lib/service.php @@ -5,13 +5,13 @@ A helper for calling remote service APIs with a natural calling sequence. Calls are automatically translated into HTTP requests based on a RESTful pattern, with smart interpretation of parameters and returned payloads. - + Without customization, the library will guess at service calls based on the pattern: - + $s->post_user_json(array('a' => 'val')); 1 2 3 4 - + 1. The HTTP method (get, put, post, delete) 2. The root level object(s) 3. The format (json, xml, etc.), optional @@ -28,19 +28,19 @@ class Service { 'get', 'put', 'post', 'delete', 'options', 'head'); public static $TYPES = array( 'json', 'xml'); - + /* Initialize a specific service */ public function __construct($options, $urlBuilder = NULL) { - + if (!function_exists('curl_init')) throw new Exception('cURL required by the Presto::Service lib.'); - + // set up the service options $this->options = (object)array_merge( array( - 'server' => '', + 'service' => '', 'username' => '', - 'referer' => '', + 'referrer' => '', 'agent' => 'PHP/Presto - Using cURL', 'type' => 'x-www-form-urlencoded', 'debug' => NULL, @@ -54,23 +54,24 @@ public function __construct($options, $urlBuilder = NULL) { $this->call = (object) array( 'uri' => '', - 'params' => array(), + 'params' => array(), 'args' => array(), 'method' => 'get', - 'cookie' => '', + 'cookie' => '', 'type' => $this->options->type, - 'ext' => '' + 'ext' => '', + 'body' => array() ); - + // set up the default URL builder - $this->urlBuilderFn = isset($urlBuilder) ? $urlBuilder + $this->urlBuilderFn = isset($urlBuilder) ? $urlBuilder : function(&$fn, &$o, &$call) { - if (empty($fn)) return false; - return "{$o->service}{$o->extra}/{$fn}{$call->id}{$call->ext}"; + if (empty($fn)) return false; + return "{$o->service}{$o->extra}/{$fn}{$call->id}{$call->ext}"; }; - + } - + /* Map service calls to service requests */ public function __call($fn, $args) { @@ -78,45 +79,54 @@ public function __call($fn, $args) { $this->call->ext = ''; $this->call->type = $this->options->type; $this->call->args = $args; - + // build arguments - $this->popArgs($args); + $this->popArgs($args); // determine function call name $fn = $this->parseCall($fn); - + // build a url from the call parameters $urlFn = $this->urlBuilderFn; $this->call->uri = $urlFn($fn, $this->options, $this->call); - + + if (!($this->call->uri)) + throw new Exception('Failed to build URL from ' + . json_encode(array( + 'service' => $this->options->service, + 'method' => $fn, + 'args' => $args))); + // set up the result object $this->result = (object) array( 'body' => '', 'data' => array() - ); + ); - $this->debug(__FUNCTION__, func_get_args()); // trace + presto_lib::_trace(__FUNCTION__, $this->call->uri); // make the actual request return $this->request(); } - // process the call arguments + + // process the call arguments private function popArgs($args) { - - $this->call->params = array(); + + $this->call->params = null; $this->call->id = ''; - + if (empty($args)) return; - + foreach ($args as $arg) { switch (gettype($arg)) { case 'object': case 'array': - $this->call->params = $arg; break; - + $this->call->params = $arg; + break; + case 'NULL': case 'unknown type': break; - + default: $parts = explode('.', $arg); $t = end($parts); @@ -124,7 +134,7 @@ private function popArgs($args) { $this->type(array_pop($parts)); $arg = str_replace(".$t", '', $arg); } - + $this->call->id .= "/$arg"; } } @@ -132,10 +142,10 @@ private function popArgs($args) { } // parse the call into a useful request private function parseCall($fn) { - + // parse the fn call $parts = explode('_', $fn); - + $t = end($parts); $m = reset($parts); @@ -150,46 +160,79 @@ private function parseCall($fn) { $path = implode('_', $parts); // reassemble the remaining path components return $path; - } + } + // perform the http request private function request() { + +// NOTE - this function needs to be refactored - DRY + $c = curl_init(); $this->call->headers = $this->options->headers; $this->call->info = null; - + // set up options specific to each HTTP method switch ($this->call->method) { - + case 'get': + // add URI parameters if any were passed to the call + $params = $this->call->params; + if (!empty($params)) { + $params = http_build_query($params); + $this->call->uri .= '?' . $params; + } break; - + + case 'put': case 'post': - curl_setopt($c, CURLOPT_POST, 1); - curl_setopt($c, CURLOPT_POSTFIELDS, $this->params()); + $opt = CURLOPT_POST; + $val = 1; + // custom request for PUT ensures POSTFIELDS as we are not PUTting a file + if ($this->call->method === 'put') { + $opt = CURLOPT_CUSTOMREQUEST; + $val = 'PUT'; + } + curl_setopt($c, $opt, $val); + if ($this->call->body) { + $body = json_encode($this->call->body); + curl_setopt($c, CURLOPT_POSTFIELDS, $body); + $params = $this->call->params; + if (!empty($params)) { + $params = http_build_query($params); + $this->call->uri .= '?' . $params; + } + } else + curl_setopt($c, CURLOPT_POSTFIELDS, $this->params()); $this->call->headers[] = $this->contentType(); break; - - case 'put': + case 'options': curl_setopt($c, CURLOPT_POST, 1); curl_setopt($c, CURLOPT_POSTFIELDS, $this->params()); $this->call->headers[] = $this->contentType(); - break; + curl_setopt($c, CURLOPT_CUSTOMREQUEST, + strtoupper($this->call->method)); + break; case 'head': case 'delete': - curl_setopt($c, CURLOPT_CUSTOMREQUEST, + $params = $this->call->params; + if (!empty($params)) { + $params = http_build_query($params); + $this->call->uri .= '?' . $params; + } + curl_setopt($c, CURLOPT_CUSTOMREQUEST, strtoupper($this->call->method)); break; - + default: - throw new Exception('Unsupported HTTP method ' + throw new Exception('Unsupported HTTP method ' + $this->call->method); } // set other HTTP otions - curl_setopt_array($c, array( + $options = array( CURLOPT_URL => $this->call->uri, CURLOPT_RETURNTRANSFER => 1, CURLOPT_TIMEOUT => 100, @@ -198,43 +241,51 @@ private function request() { CURLOPT_COOKIE => $this->call->cookie, CURLOPT_COOKIESESSION => 1, CURLOPT_HTTPHEADER => $this->call->headers, - CURLOPT_REFERER => $this->options->referer, + CURLOPT_REFERER => $this->options->referrer, CURLOPT_USERAGENT => $this->options->agent, - + CURLOPT_HEADERFUNCTION => array($this, 'header') - )); + ); + curl_setopt_array($c, $options); - $this->log('req', "{$this->call->method}: {$this->call->uri}"); + presto_lib::_trace('Service request', "{$this->call->method}: {$this->call->uri}"); + presto_lib::_trace('Service request body', json_encode($this->call->body)); $this->result->body = curl_exec($c); $this->result->uri = $this->call->uri; $this->call->info = (object)curl_getinfo($c); - $this->log('data', $this->result->body); + presto_lib::_trace('Service response body', $this->result->body); $this->parseResults(); - - if ($this->result->error = curl_error($c)) - throw new Exception($this->result->error); + + if (($this->result->error = curl_error($c))) + throw new Exception("{$this->call->info->http_code} - {$this->result->error} - " + . json_encode(array('options' => $options, 'call' => $this->call))); curl_close($c); - + if ($this->result->data === false) - throw new Exception("Data error: {$this->call->method} {$this->call->uri}", $this->call->info->http_code); + throw new Exception("{$this->call->info->http_code}: service error, no data - {$this->call->method} {$this->call->uri}", + $this->call->info->http_code); + + // TODO - handle 300-class returns? - if ($this->call->info->http_code != 200) { - $dump = ($this->options->debug) ? print_r($this->result, true) - : print_r($this->result->data, true); - throw new Exception("HTTP error\n{$this->call->method} {$this->call->uri}\n\n$dump\n" , $this->call->info->http_code); + if ($this->call->info->http_code >= 400) { + $dump = ($this->options->debug) ? json_encode($this->result) : json_encode($this->result->data); + throw new Exception("{$this->call->info->http_code} - service error in '{$this->call->method}' for '{$this->call->uri}' - $dump", $this->call->info->http_code); } - + return $this->data(); } // Get the call data (raw or processed) private function data() { return !empty($this->result->data) ? $this->result->data : $this->result->body; } public function code() { return $this->call->info->http_code; } - public function payload() { return $this->result->body; } + public function payload($body = null) { + if ($body) $this->call->body = $body; + else return $this->result->body; + } public function info() { return $this->call; } public function responseHeaders() { return $this->result->header; } public function responseHeader($k) { return !empty($this->result->header[$k]) ? $this->result->header[$k] : null; } @@ -242,115 +293,101 @@ public function responseHeader($k) { return !empty($this->result->header[$k]) ? public function details() { return print "{$this->call->method} {$this->call->uri}{$this->call->id}"; } public function opt($k,$v) { $this->options->$k = $v; } public function addHeader($k, $v) { $this->options->headers[] = "$k: $v"; } - public function setCookie($k,$v) { - $this->call->cookie .= (empty($this->call->cookie) ? '' : '; ') . $k.'='.urlencode($v); + public function setCookie($k,$v) { + $this->call->cookie .= (empty($this->call->cookie) ? '' : '; ') . $k.'='.urlencode($v); } public function cookie() { return $this->call->cookie; } - public function type($t = null) { + public function type($t = null) { if (!empty($t)) { $this->call->type = $t; $this->call->ext = ".$t"; - } + } return $this->call->type; } - - + + // Store the response headers private function header($h, $line) { - + if (preg_match('/^([^:]+):(.*)$/', $line, $parts)) { - + $hdr = trim($parts[1]); $v = trim($parts[2]); - + switch (strtolower($hdr)) { case 'set-cookie': $this->call->cookie = $v; break; default: - $this->result->header[ $hdr ] = $v; + $this->result->header[ $hdr ] = $v; } } - + return strlen($line); } - - // Process the results + + // Process the results private function parseResults() { - + if (empty($this->result->body)) return false; - + switch ($this->call->info->content_type) { case 'application/json': case 'json': $this->result->data = json_decode($this->result->body); break; - + case 'xml': case 'application/xml': libxml_use_internal_errors(true); - + if (!($this->result->data = simplexml_load_string($this->result->body))) { $detail = ''; foreach(libxml_get_errors() as $e) { - $detail .= $e->message . "\n" + $detail .= $e->message . "\n" . $this->result->body . "\n"; } throw new Exception("XML parse error:\n" . $detail, '500'); } - + break; - + default: $this->result->data = NULL; return false; } return false; } - + // render the parameters for the request private function params() { if (empty($this->call->params)) return NULL; - + switch ($this->call->type) { case 'json': $d = json_encode($this->call->params); break; - + case '': case 'x-www-form-urlencoded': $d = http_build_query($this->call->params); break; - + /* TODO: xml encoding for send payload not handled */ case 'xml': default: - throw new Exception('Unknown data format for send ' - . $this->call->type); + throw new Exception('Unknown data format for send ' + . $this->call->type); } - + return $d; } - - // Generate the content-type header - private function contentType() { - return "Content-type: application/{$this->call->type}"; - } - - private function debug($fn, $args) { - if ($this->options->debug < 1) return; - - print "$fn " - . print_r($args, true) . "\n" - . print_r(array($this->call, $this->options), true)."\n"; - } - private function log($thing, $text) { - if ($thing != $this->options->log) return; - - print "\n$text\n\n"; + // Generate the content-type header + private function contentType() { + return "Content-type: application/{$this->call->type}"; } } diff --git a/lib/session.php b/lib/session.php index 315b36b..ace025d 100644 --- a/lib/session.php +++ b/lib/session.php @@ -7,15 +7,14 @@ class session { private $t; private $cfg; + private $overrideCookie; - /* Set up session + /* Create a new session - ## Parameters - - session name - extra settings */ - public function __construct($serviceID, $settings = null) { + public function __construct($serviceID, $settings = null, $cookie = null) { + + $this->overrideCookie = $cookie; $this->cfg = array( 'cookie_name' => SERVICE_COOKIE, @@ -46,10 +45,12 @@ public function is_authorized() { if ($this->supports_api_keying()) if ($this->valid_key()) return true; - if (isset($_COOKIE[$this->cfg->cookie_name])) { + if (isset($this->overrideCookie)) + $t = urldecode($this->overrideCookie); + else if (isset($_COOKIE[$this->cfg->cookie_name])) { $t = $_COOKIE[$this->cfg->cookie_name]; } else { - if (!empty($this->cfg->token_header) && !isset($_SERVER[$this->cfg->token_header])) + if (!empty($this->cfg->token_header) && !isset($_SERVER[$this->cfg->token_header])) throw new Exception('Not authorized (no session cookie).', 401); // not signed in elseif (!empty($this->cfg->token_header)) $t = urldecode($_SERVER[$this->cfg->token_header]); @@ -94,7 +95,7 @@ public function sso_target($target, $p = null) { } /* Save the session */ - public function save($t, $r = false) { // TODO domain, secure, http-only + public function save($t, $r = false) { $expiry = $r ? 36000 : 600; if (empty($this->cfg->cookie_name) @@ -110,19 +111,34 @@ public function clear() { // get the current token public function token() { return isset($this->t) && $this->t->ok() ? $this->t->encoded() : false; } + // get info about the current user public function user() { return is_object($this->t) && $this->t->ok() ? $this->t->parts() : false; } + + // update a token based on a user record (note: hash + token will change) + public function update($u) { + unset($u->h); + $t = new auth_token((array)$u); + + if (!$t->ok()) + throw new \Exception('Session update failed (bad token)'); + + $this->t = $t; + $this->save($this->t->encoded()); + return $this->user(); + } + // can an API key work? private function supports_api_keying() { return $this->cfg->api_keyed && $this->cfg->api_key_header && array_key_exists($this->cfg->api_key_header, $_SERVER); } // is this a session for an API? private function is_api() { return class_exists('API') && class_exists('Presto'); } // is the API key valid? - private function valid_key() { + private function valid_key() { if (!array_key_exists($this->cfg->api_key_header, $_SERVER)) return false; // no API key - + $key = $_SERVER[$this->cfg->api_key_header]; // check as external key diff --git a/lib/transmogrify/info/delegation.php b/lib/transmogrify/info/delegation.php new file mode 100644 index 0000000..40d8f70 --- /dev/null +++ b/lib/transmogrify/info/delegation.php @@ -0,0 +1,24 @@ +restrictTo('json'); + return self::$req->scheme(); + } +} \ No newline at end of file diff --git a/lib/transmogrify/system/httpd.php b/lib/transmogrify/system/httpd.php new file mode 100644 index 0000000..0a31290 --- /dev/null +++ b/lib/transmogrify/system/httpd.php @@ -0,0 +1,31 @@ +restrictTo('json'); + return array('example' => 'HTTPD test'); + } + + + // Test custom header values + public function get_headers($p, $o, $b, $t) { + $this->restrictTo('json'); + + $this->status(201); + $this->add_header('CUSTOM_HEADER', 'TEST'); + return array('test' => 'ok'); + } + +} \ No newline at end of file diff --git a/lib/transmogrify/system/php.php b/lib/transmogrify/system/php.php new file mode 100644 index 0000000..fdcfba9 --- /dev/null +++ b/lib/transmogrify/system/php.php @@ -0,0 +1,46 @@ +restrictTo('json'); + return array('status' => 'fail', 'expected' => 'fail', 'invalidUTF8' => pack("H*" ,'c32e') ); + } + + // Get the PHP info on this server + public function get_info($p, $o, $b, $t) { + $this->restrictTo('html'); + return phpinfo(); + } + + // Gets php versioning and compatibility + public function get_version($p, $o, $b, $t) { + + $this->restrictTo('json'); + + $ver = explode('.', phpversion()); + $ok = ($ver[0] >= '5' && $ver[1] >= 3); + + $libs = array( + 'curl' => array( 'is_compatible' => (boolean) function_exists('curl_init'), 'version' => curl_version() ), + 'json*' => array('is_compatible' => (boolean) function_exists('json_encode'), 'version' => '' ) + ); + + return array( + 'is_compatible' => (boolean) $ok, + 'version' => $ver, + 'libs' => $libs + ); + } +} \ No newline at end of file diff --git a/examples/info.php b/lib/transmogrify/tests/setup.php similarity index 96% rename from examples/info.php rename to lib/transmogrify/tests/setup.php index cf78dae..e1ee9f6 100644 --- a/examples/info.php +++ b/lib/transmogrify/tests/setup.php @@ -1,6 +1,6 @@ output mappings. Don't use + this to generate JSON or XML (those are more easily generated with DOM->adapter patterns) + + Usage: + + print new View('login', array( 'user' => 'test-user' ) ); + + Returns the rendered text of the view or throws a status code (as a standard PHP exception). + */ class View { private $d; private $v; private $r; private $f; - public static $root; - + + public static function htmlize($v, $d) { + $v = new View($v, $d); + return $v->render(); + } + public function __construct($view = 'index', $data = null) { - + // hook view parameters - - $this->d = array('dom' => $data); // namespaced into "dom" - $this->f = $view; + + $this->d = array('dom' => $data, 'view' => $this); // namespaced into "dom" + $this->f = is_array($view) ? $view : array($view); + + return $this; } - - public function render() { + + public function render() { try { - - // verify and load view - if (!is_dir(self::$root)) self::$root = realpath(__DIR__) . '/'; - if (empty(self::$root)) self::$root = PRESTO_BASE . '../_views'; - $this->v = self::$root."/{$this->f}.php"; - - if (!is_file($this->v)) - throw new Exception("View ({$this->f}) not found in '".self::$root."'.", 501); - + // render view and return extract($this->d); ob_start(); - include($this->v); + + if (empty($this->f)) + throw new Exception('You did not specify any view paths.', 501); + + // verify and load multipart view + foreach ($this->f as $f) { + $this->v = $f; + + if (!stream_resolve_include_path($this->v)) + throw new Exception("View {$this->v} ({$this->v}) not found in: ".get_include_path().".", 404); + + include($this->v); + } + $output = ob_get_contents(); - ob_end_clean(); + ob_end_clean(); return $output; + + } catch (Exception $e) { + if ($e->getCode() === 404) throw $e; + throw new Exception('Failed to render view.', 500, $e); + } + } + + /* Include a file as a piece of a view. */ + public function load_part($p) { + try { + + extract($this->d); - // note that this could be faster (if called post-header creation in Presto) + if (stream_resolve_include_path($p) === false) + throw new Exception("Template file $p not found in: ". get_include_path() .".", 404); + include($p); + } catch (Exception $e) { - throw new Exception('Failed to render view.', 500, $e); + if ($e->getCode() === 404) throw $e; + throw new Exception('Failed to load view part $p', 500, $e); } } -} \ No newline at end of file +} diff --git a/setup-tests/.htaccess b/setup-tests/.htaccess new file mode 100644 index 0000000..8250ada --- /dev/null +++ b/setup-tests/.htaccess @@ -0,0 +1,19 @@ +Options +FollowSymlinks + + + +RewriteEngine On +RewriteBase / + +# Route API requests +RewriteCond %{REQUEST_FILENAME} !-f +RewriteRule (.*)\.(.*)$ /setup-tests/delegator-index.php?r=$1&t=$2 [L,QSA] + +# General requests (enable for standard php/index delegate behaviour) +RewriteCond %{REQUEST_FILENAME} !-f +RewriteRule (.*) /setup-tests/index.php?presto&$1 [L] + + + +ErrorDocument 404 /setup-tests/index.php?presto&e=404&$1 +ErrorDocument 500 /setup-tests/index.php?presto&e=500&$1 \ No newline at end of file diff --git a/examples/delegator.php b/setup-tests/delegator-index.php similarity index 100% rename from examples/delegator.php rename to setup-tests/delegator-index.php diff --git a/setup-tests/inc.php b/setup-tests/inc.php new file mode 120000 index 0000000..028bea4 --- /dev/null +++ b/setup-tests/inc.php @@ -0,0 +1 @@ +../lib/inc.php \ No newline at end of file diff --git a/setup-tests/index.php b/setup-tests/index.php new file mode 100644 index 0000000..7f00cb4 --- /dev/null +++ b/setup-tests/index.php @@ -0,0 +1,241 @@ + + + + + + + + + + + + + +Presto - installation checklist + + + + + + + + + + + + +Fork me on GitHub + +
    +

    Presto Install checklist

    + + + +
    + + +
    + +
    += '5' && $ver[1] >= 3); +?> + PHP version ok? +

    Found PHP version

    +

    PHP execution environment?

    +
    + +
    + + cURL PHP installed? +

    cURL details:

    +

    cURL is missing from your PHP installation.

    +
    + +
    + + JSON PHP installed? +

    Determines if JSON support is enabled.

    +
    + + +
    + + HTACCESS enabled? +

    Validates that both .htaccess rules and non-API delegation are functional.

    +
    Missing Presto tag in GET request for this page.
    +
    +$_GET 
    +
    +
    + + +
    + Presto delegation ok? +

    Validates basic Presto delegation. This test looks for a valid Presto 404.

    + + +
    + +
    + Presto delegate execution? +

    Validates sample Presto delegation.

    + + +
    + +
    + Presto header magic? +

    Validates Presto header functions.

    + + +
    + +
    + + Presto debug mode +

    Debug mode adds trace and informational logging via syslog and extra detail in exceptions.

    +
    + +
    +Request details +
    +
    +
    +
    +
    +
    +
    + + +
    +
    + + + + + + + + + + + + + diff --git a/setup-tests/info.php b/setup-tests/info.php new file mode 100644 index 0000000..bd7accb --- /dev/null +++ b/setup-tests/info.php @@ -0,0 +1,71 @@ +_.. +*/ +class info extends API { + + public function __construct() { + parent::__construct('presto-example-1'); + // other startup would go here + } + + // info.json (root get request) + public function get($p) { + + $this->restrictTo(array('json', 'js')); + + if (count($p) > 1) + throw new Exception('Too many parameters', 400); // will result in a proper 400 HTTP status + + return array('example' => 'This is some example information'); // will be returned as json, if json is requested + } + + // Test custom header values + public function get_header_test($ctx) { + $this->restrictTo(array('json', 'js')); + + $this->status(201); + $this->add_header('CUSTOM_HEADER', 'TEST'); + return array('test' => 'ok'); + } + + // Test binary json values (this should fail) + public function get_utf8($ctx) { + + $this->restrictTo(array('json', 'js')); + return array('status' => 'fail', 'expected' => 'fail', 'invalidUTF8' => pack("H*" ,'c32e') ); + } + + // Get the PHP version on this server + public function get_php_version() { + return array(phpinfo()); + } + + // Get a test image + public function get_image($p, $o, $b, $t) { + $this->restrictTo(array('png')); + $this->add_header('Content-Type', 'image/png'); + + return (string) file_get_contents('test-image.png'); + } + + // Tests a s2s call + public function get_service_test($p, $o, $b, $t) { + $s = new service(array( + 'service' => 'http://presto.test/setup-tests/' + )); + + $d = $s->get_info('header_test.json'); + + return array('status' => 'ok', 's2s' => $s); + } +} \ No newline at end of file diff --git a/setup-tests/test-image.png b/setup-tests/test-image.png new file mode 100644 index 0000000..085b5a8 Binary files /dev/null and b/setup-tests/test-image.png differ