From a2f3100ea5a819e225f1d4e348f29ca059465e82 Mon Sep 17 00:00:00 2001 From: mayue Date: Mon, 15 Nov 2021 16:18:26 +0800 Subject: [PATCH 01/42] * Modify branch. --- module/execution/model.php | 10 +++++- module/product/model.php | 56 +++++++++++++++++++++++++------ module/project/control.php | 6 ++-- module/project/view/view.html.php | 12 ++++--- 4 files changed, 65 insertions(+), 19 deletions(-) diff --git a/module/execution/model.php b/module/execution/model.php index 54c37ce3e8..3668ac5b05 100644 --- a/module/execution/model.php +++ b/module/execution/model.php @@ -3681,7 +3681,15 @@ class executionModel extends model $productPlans = array(); foreach($products as $productID => $product) { - $productPlans[$productID] = $this->productplan->getPairs($product->id, isset($product->branch) ? $product->branch : ''); + foreach($product->branch as $branchID) + { + $planInfo = $this->productplan->getPairs($product->id, $branchID); + foreach($planInfo as $planID => $plan) + { + if(empty($planID)) continue; + $productPlans[$productID][$planID] = $plan; + } + } } return $productPlans; } diff --git a/module/product/model.php b/module/product/model.php index f3a729e875..b4a0ada3a6 100644 --- a/module/product/model.php +++ b/module/product/model.php @@ -333,18 +333,52 @@ class productModel extends model * @access public * @return array */ - public function getProducts($projectID = 0, $status = 'all', $orderBy = '') + public function getProducts($projectID = 0, $status = 'all', $orderBy = '', $test = false) { - return $this->dao->select('t1.*, t2.branch, t2.plan') - ->from(TABLE_PRODUCT)->alias('t1') - ->leftJoin(TABLE_PROJECTPRODUCT)->alias('t2') - ->on('t1.id = t2.product') - ->where('t1.deleted')->eq(0) - ->beginIF(!empty($projectID))->andWhere('t2.project')->eq($projectID)->fi() - ->beginIF(!$this->app->user->admin)->andWhere('t1.id')->in($this->app->user->view->products)->fi() - ->beginIF(strpos($status, 'noclosed') !== false)->andWhere('status')->ne('closed')->fi() - ->orderBy($orderBy . 't1.order asc') - ->fetchAll('id'); + if($test) + { + $productList = $this->dao->select('t1.*, t2.branch, t2.plan') + ->from(TABLE_PRODUCT)->alias('t1') + ->leftJoin(TABLE_PROJECTPRODUCT)->alias('t2') + ->on('t1.id = t2.product') + ->where('t1.deleted')->eq(0) + ->beginIF(!empty($projectID))->andWhere('t2.project')->eq($projectID)->fi() + ->beginIF(!$this->app->user->admin)->andWhere('t1.id')->in($this->app->user->view->products)->fi() + ->beginIF(strpos($status, 'noclosed') !== false)->andWhere('status')->ne('closed')->fi() + ->orderBy($orderBy . 't1.order asc') + ->fetchGroup('id', 'branch'); + + $products = array(); + foreach($productList as $productID => $branches) + { + $branch = array(); + $plan = array(); + foreach($branches as $branchID => $productInfo) + { + $branch[$productInfo->branch] = $productInfo->branch; + $plan[$productInfo->plan] = $productInfo->plan; + + $productInfo->branch = $branch; + $productInfo->plan = $plan; + + $products[$productID] = $productInfo; + } + } + return $products; + } + else + { + $productList = $this->dao->select('t1.*, t2.branch, t2.plan') + ->from(TABLE_PRODUCT)->alias('t1') + ->leftJoin(TABLE_PROJECTPRODUCT)->alias('t2') + ->on('t1.id = t2.product') + ->where('t1.deleted')->eq(0) + ->beginIF(!empty($projectID))->andWhere('t2.project')->eq($projectID)->fi() + ->beginIF(!$this->app->user->admin)->andWhere('t1.id')->in($this->app->user->view->products)->fi() + ->beginIF(strpos($status, 'noclosed') !== false)->andWhere('status')->ne('closed')->fi() + ->orderBy($orderBy . 't1.order asc') + ->fetchAll('id'); + } } /** diff --git a/module/project/control.php b/module/project/control.php index 1f87498a8a..15519ac9b9 100644 --- a/module/project/control.php +++ b/module/project/control.php @@ -663,11 +663,11 @@ class project extends control $this->project->setMenu($projectID); - $products = $this->loadModel('product')->getProducts($projectID); + $products = $this->loadModel('product')->getProducts($projectID, 'all', '', true); $linkedBranches = array(); - foreach($products as $product) + foreach($products as $productID => $product) { - if($product->branch) $linkedBranches[$product->branch] = $product->branch; + if($product->branch) $linkedBranches = $product->branch; } /* Load pager. */ diff --git a/module/project/view/view.html.php b/module/project/view/view.html.php index aa5276502f..daf52bfb5a 100644 --- a/module/project/view/view.html.php +++ b/module/project/view/view.html.php @@ -140,11 +140,13 @@
$product):?> - branch]) ? '/' . $branchGroups[$productID][$product->branch] : '';?> + branch as $branchID):?> +
- createLink('product', 'browse', "productID=$productID&branch=$product->branch"), " " . $product->name . $branchName);?> + createLink('product', 'browse', "productID=$productID&branch=$branchID"), " " . $product->name . $branchName);?>
+
@@ -153,10 +155,12 @@
$product):?> - plan])):?> -
createLink('productplan', 'view', "planID={$product->plan}"), $product->name . '/' . $planGroup[$productID][$product->plan]);?>
+ plan as $planID):?> + +
createLink('productplan', 'view', "planID={$planID}"), $product->name . '/' . $planGroup[$productID][$planID]);?>
+
From d268587e8df03d72f177ca98e413e1a3008d92f1 Mon Sep 17 00:00:00 2001 From: tianshujie Date: Tue, 16 Nov 2021 15:07:18 +0800 Subject: [PATCH 02/42] * Finish task #44496. --- module/story/control.php | 35 ++++++++++++++++++++----- module/story/view/batchedit.html.php | 9 +++---- module/testcase/control.php | 30 ++++++++++++++++----- module/testcase/view/batchedit.html.php | 6 ++--- 4 files changed, 58 insertions(+), 22 deletions(-) diff --git a/module/story/control.php b/module/story/control.php index 9946bdcfa8..2c77f443e7 100644 --- a/module/story/control.php +++ b/module/story/control.php @@ -711,14 +711,35 @@ class story extends control $product = $this->product->getByID($productID); $branchProduct = $product->type == 'normal' ? false : true; - /* Set modules. */ - $modules = array('ditto' => $this->lang->story->ditto) + $this->tree->getOptionMenu($productID, $viewType = 'story', 0, $branch); + /* Set branches and modules. */ + $branches = array(); + $modules = array(); + if($product->type != 'normal') + { + $branches = $this->loadModel('branch')->getPairs($productID); + if($branch === 'all') + { + $modules[0] = $this->tree->getOptionMenu($productID, $viewType = 'story', 0, $branch); + foreach($branches as $branchID => $branchName) + { + $modules[$branchID] = $this->tree->getOptionMenu($productID, $viewType = 'story', 0, $branchID); + } + } + else + { + $modules[$branch] = $this->tree->getOptionMenu($productID, $viewType = 'story', 0, $branch); + } + } + else + { + $modules[0] = $this->tree->getOptionMenu($productID, $viewType = 'story', 0, $branch); + } - $this->view->modules = $modules; - $this->view->branches = $product->type == 'normal' ? array() : $this->loadModel('branch')->getPairs($product->id); - $this->view->plans = $this->productplan->getBranchPlanPairs($productID); - $this->view->position[] = html::a($this->createLink('product', 'browse', "product=$product->id&branch=$branch"), $product->name); - $this->view->title = $product->name . $this->lang->colon . $this->lang->story->batchEdit; + $this->view->modules = $modules; + $this->view->branches = $branches; + $this->view->plans = $this->productplan->getBranchPlanPairs($productID); + $this->view->position[] = html::a($this->createLink('product', 'browse', "product=$product->id&branch=$branch"), $product->name); + $this->view->title = $product->name . $this->lang->colon . $this->lang->story->batchEdit; } elseif($executionID) { diff --git a/module/story/view/batchedit.html.php b/module/story/view/batchedit.html.php index b789d3567e..5e3f65d5cb 100644 --- a/module/story/view/batchedit.html.php +++ b/module/story/view/batchedit.html.php @@ -76,15 +76,12 @@ foreach(explode(',', $showFields) as $field) if($product->type != 'normal') { foreach($branches as $branchID => $branchName) $branches[$branchID] = '/' . $product->name . '/' . $branchName; - $branches = array('ditto' => $this->lang->story->ditto) + $branches; } - $modules = $this->tree->getOptionMenu($story->product, $viewType = 'story', 0, $story->branch); - foreach($modules as $moduleID => $moduleName) $modules[$moduleID] = '/' . $product->name . $moduleName; - $modules = array('ditto' => $this->lang->story->ditto) + $modules; + if(!isset($modules[$story->branch])) $modules[$story->branch] = $this->tree->getOptionMenu($story->product, $viewType = 'story', 0, $story->branch); + foreach($modules[$story->branch] as $moduleID => $moduleName) $modules[$story->branch][$moduleID] = '/' . $product->name . $moduleName; $productPlans = $this->productplan->getPairs($story->product, $branch); - $productPlans = array('' => '', 'ditto' => $this->lang->story->ditto) + $productPlans; } ?> @@ -97,7 +94,7 @@ foreach(explode(',', $showFields) as $field) '> - module, "class='form-control chosen'");?> + branch], $story->module, "class='form-control chosen'");?> '> tree->getOptionMenu($libID, $viewType = 'caselib', $startModuleID = 0, $branch); - $modules = array('ditto' => $this->lang->testcase->ditto) + $modules; $this->view->modules = $modules; $this->view->title = $libraries[$libID] . $this->lang->colon . $this->lang->testcase->batchEdit; @@ -877,11 +876,31 @@ class testcase extends control if($product->type != 'normal') $branchProduct = true; - /* Set modules. */ - $modules = $this->tree->getOptionMenu($productID, $viewType = 'case', $startModuleID = 0, $branch); - $modules = array('ditto' => $this->lang->testcase->ditto) + $modules; + /* Set branches and modules. */ + $branches = array(); + $modules = array(); + if($product->type != 'normal') + { + $branches = $this->loadModel('branch')->getPairs($productID); + if($branch === 'all') + { + $modules[0] = $this->tree->getOptionMenu($productID, $viewType = 'case', 0, $branch); + foreach($branches as $branchID => $branchName) + { + $modules[$branchID] = $this->tree->getOptionMenu($productID, $viewType = 'case', 0, $branchID); + } + } + else + { + $modules[$branch] = $this->tree->getOptionMenu($productID, $viewType = 'case', 0, $branch); + } + } + else + { + $modules[0] = $this->tree->getOptionMenu($productID, $viewType = 'case', 0, $branch); + } - $this->view->branches = $product->type == 'normal' ? array() : $this->loadModel('branch')->getPairs($product->id); + $this->view->branches = $branches; $this->view->modules = $modules; $this->view->position[] = html::a($this->createLink('testcase', 'browse', "productID=$productID"), $this->products[$productID]); $this->view->title = $product->name . $this->lang->colon . $this->lang->testcase->batchEdit; @@ -906,7 +925,6 @@ class testcase extends control $this->view->position[] = html::a($this->server->http_referer, $this->lang->my->testCase); $this->view->title = $this->lang->testcase->batchEdit; - /* Set modules. */ $productIdList = array(); foreach($cases as $case) $productIdList[$case->product] = $case->product; diff --git a/module/testcase/view/batchedit.html.php b/module/testcase/view/batchedit.html.php index 4095b21ddf..88762552cf 100644 --- a/module/testcase/view/batchedit.html.php +++ b/module/testcase/view/batchedit.html.php @@ -67,6 +67,7 @@ branch; if((!$productID and !$cases[$caseID]->lib) or $app->tab != 'qa') { $product = $this->product->getByID($cases[$caseID]->product); @@ -74,10 +75,9 @@ if($product->type != 'normal') { foreach($branches as $branchID => $branchName) $branches[$branchID] = '/' . $product->name . '/' . $branchName; - $branches = array('ditto' => $this->lang->story->ditto) + $branches; } - $modules = $this->tree->getOptionMenu($cases[$caseID]->product, $viewType = 'case', 0, $cases[$caseID]->branch); + $modules[$caseBranch] = $this->tree->getOptionMenu($cases[$caseID]->product, $viewType = 'case', 0, $caseBranch); } ?> @@ -103,7 +103,7 @@ branch, "class='form-control chosen' onchange='loadBranches($branchProductID, this.value, $caseID)', $disabled");?> - ' style='overflow:visible'>module, "class='form-control chosen'");?> + ' style='overflow:visible'>module, "class='form-control chosen'");?> ' style='overflow:visible'>story, "class='form-control chosen'");?>
From fdfadb794013c287b6414261e4b032791eec9f96 Mon Sep 17 00:00:00 2001 From: tianshujie Date: Tue, 16 Nov 2021 15:15:14 +0800 Subject: [PATCH 03/42] * Modify the wrong parameter. --- module/testcase/control.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/module/testcase/control.php b/module/testcase/control.php index 998577a1eb..01fdfb5e10 100644 --- a/module/testcase/control.php +++ b/module/testcase/control.php @@ -884,7 +884,7 @@ class testcase extends control $branches = $this->loadModel('branch')->getPairs($productID); if($branch === 'all') { - $modules[0] = $this->tree->getOptionMenu($productID, $viewType = 'case', 0, $branch); + $modules[0] = $this->tree->getOptionMenu($productID, $viewType = 'case', 0, 0); foreach($branches as $branchID => $branchName) { $modules[$branchID] = $this->tree->getOptionMenu($productID, $viewType = 'case', 0, $branchID); @@ -897,7 +897,7 @@ class testcase extends control } else { - $modules[0] = $this->tree->getOptionMenu($productID, $viewType = 'case', 0, $branch); + $modules[0] = $this->tree->getOptionMenu($productID, $viewType = 'case', 0, 0); } $this->view->branches = $branches; From fdade49b5d1d55a1d5a9e1c6c65edc5a647d5455 Mon Sep 17 00:00:00 2001 From: tianshujie Date: Wed, 17 Nov 2021 09:21:05 +0800 Subject: [PATCH 04/42] * Fix the creation and editing of stories and bugs. --- module/bug/css/create.css | 1 + module/bug/js/batchcreate.js | 11 +++++++++++ module/bug/view/edit.html.php | 2 +- module/product/control.php | 4 ++-- module/productplan/control.php | 2 +- module/story/js/batchcreate.js | 11 +++++++++++ 6 files changed, 27 insertions(+), 4 deletions(-) diff --git a/module/bug/css/create.css b/module/bug/css/create.css index 433e87435a..de274bd781 100644 --- a/module/bug/css/create.css +++ b/module/bug/css/create.css @@ -45,3 +45,4 @@ html[lang='en'] #deadlineTd .input-group-addon {padding: 5px 18px;} #osBox .required:after {right: 1px;} #osBox {width: 190px;} #projectBox .required:after {right: 1px;} +#branch_chosen {min-width: 90px;} diff --git a/module/bug/js/batchcreate.js b/module/bug/js/batchcreate.js index 41834f78d2..a7ae9e9e8a 100644 --- a/module/bug/js/batchcreate.js +++ b/module/bug/js/batchcreate.js @@ -39,6 +39,17 @@ function setBranchRelated(branchID, productID, num) buildLink = createLink('build', 'ajaxGetProductBuilds', 'productID=' + productID + "&varName=openedBuilds&build=&branch=" + branchID + "&index=" + num); + /* If the branch of the current row is inconsistent with the one below, clear the module and execution of the nex row. */ + var nextBranchID = $('#branch' + (num + 1)).val(); + if(nextBranchID != branchID) + { + $('#modules' + (num + 1)).find("option[value='ditto']").remove(); + $('#modules' + (num + 1)).trigger("chosen:updated"); + + $('#executions' + (num + 1)).find("option[value='ditto']").remove(); + $('#executions' + (num + 1)).trigger("chosen:updated"); + } + setOpenedBuilds(buildLink, num); } diff --git a/module/bug/view/edit.html.php b/module/bug/view/edit.html.php index 552d3e2002..ae065ded78 100644 --- a/module/bug/view/edit.html.php +++ b/module/bug/view/edit.html.php @@ -19,7 +19,7 @@ js::set('changeProductConfirmed' , false); js::set('changeExecutionConfirmed' , false); js::set('confirmChangeProduct' , $lang->bug->confirmChangeProduct); js::set('planID' , $bug->plan); -js::set('oldExecutionID' , $bug->execution); +js::set('oldProjectID' , $bug->project); js::set('oldStoryID' , $bug->story); js::set('oldTaskID' , $bug->task); js::set('oldOpenedBuild' , $bug->openedBuild); diff --git a/module/product/control.php b/module/product/control.php index f1628fdda8..38a40b0eeb 100644 --- a/module/product/control.php +++ b/module/product/control.php @@ -929,7 +929,7 @@ class product extends control public function ajaxGetProjects($productID, $branch = 0, $projectID = 0) { $projects = array('' => ''); - $projects += $this->product->getProjectPairsByProduct($productID, $branch ? "0,$branch" : $branch); + $projects += $this->product->getProjectPairsByProduct($productID, $branch); if($this->app->getViewType() == 'json') die(json_encode($projects)); die(html::select('project', $projects, $projectID, "class='form-control' onchange='loadProductExecutions({$productID}, this.value)'")); @@ -948,7 +948,7 @@ class product extends control */ public function ajaxGetExecutions($productID, $projectID = 0, $branch = 0, $number = '', $executionID = 0) { - $executions = $this->product->getExecutionPairsByProduct($productID, $branch ? "0,$branch" : $branch, 'id_desc', $projectID); + $executions = $this->product->getExecutionPairsByProduct($productID, $branch, 'id_desc', $projectID); if($this->app->getViewType() == 'json') die(json_encode($executions)); if($number === '') diff --git a/module/productplan/control.php b/module/productplan/control.php index 4b4f947742..b5f8500776 100644 --- a/module/productplan/control.php +++ b/module/productplan/control.php @@ -346,7 +346,7 @@ class productplan extends control } else { - $plans = $this->productplan->getPairs($productID, $branch, $expired); + $plans = $this->productplan->getPairs($productID, $branch); } $planName = $number === '' ? 'plan' : "plan[$number]"; diff --git a/module/story/js/batchcreate.js b/module/story/js/batchcreate.js index 412811809f..98da6e91d8 100644 --- a/module/story/js/batchcreate.js +++ b/module/story/js/batchcreate.js @@ -67,6 +67,17 @@ function setModuleAndPlan(branchID, productID, num) $("#plan" + num).next('.picker').remove(); $("#plan" + num).chosen(); }); + + /* If the branch of the current row is inconsistent with the one below, clear the module and plan of the nex row. */ + var nextBranchID = $('#branch' + (num + 1)).val(); + if(nextBranchID != branchID) + { + $('#module' + (num + 1)).find("option[value='ditto']").remove(); + $('#module' + (num + 1)).trigger("chosen:updated"); + + $('#plan' + (num + 1)).find("option[value='ditto']").remove(); + $('#plan' + (num + 1)).trigger("chosen:updated"); + } } /* Copy story title as story spec. */ From 859327d7a35d7aad8b4e2b6f89648e5bafaad79d Mon Sep 17 00:00:00 2001 From: tianshujie Date: Wed, 17 Nov 2021 14:50:42 +0800 Subject: [PATCH 05/42] * Modify the logic of loading stories when creating use cases in batches. --- module/story/control.php | 11 +++- module/story/model.php | 5 +- module/testcase/control.php | 5 +- module/testcase/js/batchcreate.js | 75 +++++------------------ module/testcase/js/common.js | 23 +++++++ module/testcase/view/batchcreate.html.php | 4 +- 6 files changed, 52 insertions(+), 71 deletions(-) diff --git a/module/story/control.php b/module/story/control.php index 2c77f443e7..d65168cc40 100644 --- a/module/story/control.php +++ b/module/story/control.php @@ -1828,15 +1828,20 @@ class story extends control * AJAX: get stories of a product in html select. * * @param int $productID + * @param int $branch * @param int $moduleID * @param int $storyID * @param string $onlyOption * @param string $status * @param int $limit + * @param string $type + * @param bool $hasParent + * @param int $executionID + * @param int $number * @access public * @return void */ - public function ajaxGetProductStories($productID, $branch = 0, $moduleID = 0, $storyID = 0, $onlyOption = 'false', $status = '', $limit = 0, $type = 'full', $hasParent = 1, $executionID = 0) + public function ajaxGetProductStories($productID, $branch = 0, $moduleID = 0, $storyID = 0, $onlyOption = 'false', $status = '', $limit = 0, $type = 'full', $hasParent = 1, $executionID = 0, $number = '') { if($moduleID) { @@ -1858,11 +1863,11 @@ class story extends control } else { - $stories = $this->story->getProductStoryPairs($productID, $branch ? "0,$branch" : $branch, $moduleID, $storyStatus, 'id_desc', $limit, $type, 'story', $hasParent); + $stories = $this->story->getProductStoryPairs($productID, $branch, $moduleID, $storyStatus, 'id_desc', $limit, $type, 'story', $hasParent); } $storyID = isset($stories[$storyID]) ? $storyID : 0; - $select = html::select('story', empty($stories) ? array('' => '') : $stories, $storyID, "class='form-control'"); + $select = html::select('story' . $number, empty($stories) ? array('' => '') : $stories, $storyID, "class='form-control'"); /* If only need options, remove select wrap. */ if($onlyOption == 'true') die(substr($select, strpos($select, '>') + 1, -10)); diff --git a/module/story/model.php b/module/story/model.php index 5d51043f70..22ddb25fcf 100644 --- a/module/story/model.php +++ b/module/story/model.php @@ -2228,13 +2228,12 @@ class storyModel extends model */ public function getProductStoryPairs($productID = 0, $branch = 0, $moduleIdList = 0, $status = 'all', $order = 'id_desc', $limit = 0, $type = 'full', $storyType = 'story', $hasParent = true) { - if($branch) $branch = "0,$branch";//Fix bug 1059. $stories = $this->dao->select('t1.id, t1.title, t1.module, t1.pri, t1.estimate, t2.name AS product') ->from(TABLE_STORY)->alias('t1')->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t1.product = t2.id') ->where('1=1') ->beginIF($productID)->andWhere('t1.product')->in($productID)->fi() ->beginIF($moduleIdList)->andWhere('t1.module')->in($moduleIdList)->fi() - ->beginIF($branch)->andWhere('t1.branch')->in($branch)->fi() + ->beginIF($branch !== 'all')->andWhere('t1.branch')->in($branch)->fi() ->beginIF(!$hasParent)->andWhere('t1.parent')->ge(0)->fi() ->beginIF($status and $status != 'all')->andWhere('t1.status')->in($status)->fi() ->andWhere('t1.deleted')->eq(0) @@ -2673,7 +2672,7 @@ class storyModel extends model ->where('t1.project')->eq((int)$executionID) ->andWhere('t2.deleted')->eq(0) ->beginIF($productID)->andWhere('t2.product')->eq((int)$productID)->fi() - ->beginIF($branch)->andWhere('t2.branch')->in("0,$branch")->fi() + ->beginIF($branch != 'all')->andWhere('t2.branch')->eq($branch)->fi() ->beginIF($moduleIdList)->andWhere('t2.module')->in($moduleIdList)->fi() ->beginIF($status == 'unclosed')->andWhere('t2.status')->ne('closed')->fi() ->orderBy('t1.`order` desc') diff --git a/module/testcase/control.php b/module/testcase/control.php index 01fdfb5e10..73501f4046 100644 --- a/module/testcase/control.php +++ b/module/testcase/control.php @@ -522,8 +522,9 @@ class testcase extends control $this->app->tab == 'project' ? $this->loadModel('project')->setMenu($this->session->project) : $this->testcase->setMenu($this->products, $productID, $branch); /* Set story list. */ - $story = $storyID ? $this->story->getByID($storyID) : ''; - $storyList = $storyID ? array($storyID => $story->id . ':' . $story->title) : array(''); + $story = $storyID ? $this->story->getByID($storyID) : ''; + $storyList = $this->loadModel('story')->getProductStoryPairs($productID, $branch); + $storyList += $storyID ? array($storyID => $story->id . ':' . $story->title) : array(''); /* Set module option menu. */ $moduleOptionMenu = $this->tree->getOptionMenu($productID, $viewType = 'case', $startModuleID = 0, $branch === 'all' ? 0 : $branch); diff --git a/module/testcase/js/batchcreate.js b/module/testcase/js/batchcreate.js index adfc541b4f..421ffc79f9 100644 --- a/module/testcase/js/batchcreate.js +++ b/module/testcase/js/batchcreate.js @@ -3,66 +3,6 @@ $(document).ready(function() removeDitto();//Remove 'ditto' in first row. if($('#batchCreateForm table thead tr th.c-title').width() < 150) $('#batchCreateForm table thead tr th.c-title').width('150'); - $(document).on('mouseup', '.chosen-with-drop', function() - { - var select = $(this).prev('select'); - var id = $(select).attr('id'); - if(id.indexOf('story') != -1) - { - var index = id.substring(5); - var moduleID = $('#module' + index).val(); - var branch = $('#branch' + index).length > 0 ? $('#branch' + index).val() : 0; - if(moduleID == 'ditto') - { - for(var i = index - 1; i >=0; i--) - { - if($('#module' + i).val() != 'ditto') - { - moduleID = $('#module' + i).val(); - break; - } - } - } - var link = createLink('story', 'ajaxGetProductStories', 'productID=' + productID + '&branch=' + branch + '&moduleID=' + moduleID + '&storyID='+ ($(select).val() || '0') + '&onlyOption=true&status=noclosed&limit=0&type=null&hasParent=0'); - var $story = $('#story' + index); - if($story.data('loadLink') !== link) - { - $story.load(link, function(){$story.data('loadLink', link).trigger("chosen:updated");}); - } - } - if($(select).val() == 'ditto') - { - var index = $(select).closest('td').index(); - var row = $(select).closest('tr').index(); - var table = $(select).closest('tr').parent(); - var value = ''; - for(i = row - 1; i >= 0; i--) - { - value = $(table).find('tr').eq(i).find('td').eq(index).find('select').val(); - if(value != 'ditto') break; - } - $(select).val(value); - $(select).trigger("chosen:updated"); - } - }); - - $(document).on('mousedown', 'select', function() - { - if($(this).val() == 'ditto') - { - var index = $(this).closest('td').index(); - var row = $(this).closest('tr').index(); - var table = $(this).closest('tr').parent(); - var value = ''; - for(i = row - 1; i >= 0; i--) - { - value = $(table).find('tr').eq(i).find('td').eq(index).find('select').val(); - if(value != 'ditto') break; - } - $(this).val(value); - } - }); - $(document).keydown(function(event) { if(event.ctrlKey && event.keyCode == 38) @@ -106,6 +46,19 @@ function setModules(branchID, productID, num) $('#module' + num).replaceWith(modules); $("#module" + num + "_chosen").remove(); $("#module" + num).next('.picker').remove(); - $("#module" + num).chosen(); + $("#module" + num).attr('onchange', "loadStories("+ productID + ", this.value, " + num + ")").chosen(); }); + + loadStories(productID, 0, num); + + /* If the branch of the current row is inconsistent with the one below, clear the module and story of the nex row. */ + var nextBranchID = $('#branch' + (num + 1)).val(); + if(nextBranchID != branchID) + { + $('#module' + (num + 1)).find("option[value='ditto']").remove(); + $('#module' + (num + 1)).trigger("chosen:updated"); + + $('#plan' + (num + 1)).find("option[value='ditto']").remove(); + $('#plan' + (num + 1)).trigger("chosen:updated"); + } } diff --git a/module/testcase/js/common.js b/module/testcase/js/common.js index 5da8863fb8..38e2ae74ce 100644 --- a/module/testcase/js/common.js +++ b/module/testcase/js/common.js @@ -343,3 +343,26 @@ function updateStepID() var i = 1; $('.stepID').each(function(){$(this).html(i ++)}); } + +/** + * Set stories. + * + * @param int productID + * @param int moduleID + * @param int num + * @access public + * @return void + */ +function loadStories(productID, moduleID, num) +{ + var branchID = $('#branch' + num).val(); + var storyLink = createLink('story', 'ajaxGetProductStories', 'productID=' + productID + '&branch=' + branchID + '&moduleID=' + moduleID + '&storyID=0&onlyOption=false&status=noclosed&limit=50&type=full&hasParent=1&executionID=0&number=' + num); + $.get(storyLink, function(stories) + { + if(!stories) modules = ''; + $('#story' + num).replaceWith(stories); + $("#story" + num + "_chosen").remove(); + $("#story" + num).next('.picker').remove(); + $("#story" + num).chosen(); + }); +} diff --git a/module/testcase/view/batchcreate.html.php b/module/testcase/view/batchcreate.html.php index 3b76d24e83..a5c29bf3d0 100644 --- a/module/testcase/view/batchcreate.html.php +++ b/module/testcase/view/batchcreate.html.php @@ -83,7 +83,7 @@ '> - ' style='overflow:visible'> + ' style='overflow:visible'> ' style='overflow:visible'> id : '', 'class="form-control chosen"');?>
@@ -124,7 +124,7 @@ %s '> - ' style='overflow:visible'> + ' style='overflow:visible'> ' style='overflow:visible'>
From 1dab48045ec7463da5a8aa7d37d87576aaad8965 Mon Sep 17 00:00:00 2001 From: songchenxuan Date: Wed, 17 Nov 2021 15:39:30 +0800 Subject: [PATCH 06/42] * Finish task 44490. --- module/bug/control.php | 2 +- module/bug/view/create.html.php | 1 + module/build/model.php | 31 +++++++++++++++++++++++++++++-- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/module/bug/control.php b/module/bug/control.php index 1b5787c1a5..885fb495a1 100644 --- a/module/bug/control.php +++ b/module/bug/control.php @@ -207,7 +207,7 @@ class bug extends control $showModule = !empty($this->config->datatable->bugBrowse->showModule) ? $this->config->datatable->bugBrowse->showModule : ''; $productName = ($productID and isset($this->products[$productID])) ? $this->products[$productID] : $this->lang->product->allProduct; - + $product = $this->product->getById($productID); if($product and $product->type != 'normal') { diff --git a/module/bug/view/create.html.php b/module/bug/view/create.html.php index 2bbe2f801c..066be43156 100644 --- a/module/bug/view/create.html.php +++ b/module/bug/view/create.html.php @@ -120,6 +120,7 @@ js::set('moduleID', $moduleID);
bug->openedBuild?> +
bug->allBuilds, "class='btn' id='all' data-toggle='tooltip' onclick='loadAllBuilds()'")?>
diff --git a/module/build/model.php b/module/build/model.php index 8a556bf792..fea109c4dd 100644 --- a/module/build/model.php +++ b/module/build/model.php @@ -269,6 +269,7 @@ class buildModel extends model */ public function getProductBuildPairs($products, $branch = 0, $params = 'noterminate, nodone', $replace = true) { + if($branch == 'all') $branch = 0; $sysBuilds = array(); if(strpos($params, 'noempty') === false) $sysBuilds = array('' => ''); if(strpos($params, 'notrunk') === false) $sysBuilds = $sysBuilds + array('trunk' => $this->lang->trunk); @@ -278,10 +279,24 @@ class buildModel extends model ->leftJoin(TABLE_RELEASE)->alias('t3')->on('t1.id = t3.build') ->leftJoin(TABLE_BRANCH)->alias('t4')->on('t1.branch = t4.id') ->where('t1.product')->in($products) - ->beginIF($branch)->andWhere('t1.branch')->in("0,$branch")->fi() + ->beginIF($branch)->andWhere('t1.branch')->in("$branch")->fi() ->andWhere('t1.deleted')->eq(0) ->orderBy('t1.date desc, t1.id desc')->fetchAll('id'); + $productBuildss = array(); + if($branch != 0) + { + $productBuildss = $this->dao->select('t1.id, t1.name, t1.execution, t2.status as executionStatus, t3.id as releaseID, t3.status as releaseStatus, t4.name as branchName')->from(TABLE_BUILD)->alias('t1') + ->leftJoin(TABLE_EXECUTION)->alias('t2')->on('t1.execution = t2.id') + ->leftJoin(TABLE_RELEASE)->alias('t3')->on('t1.id = t3.build') + ->leftJoin(TABLE_BRANCH)->alias('t4')->on('t1.branch = t4.id') + ->where('t1.product')->in($products) + ->beginIF($branch)->andWhere('t1.branch')->in("0")->fi() + ->andWhere('t1.deleted')->eq(0) + ->orderBy('t1.date desc, t1.id desc')->fetchAll('id'); + } + + /* Set builds and filter done executions and terminate releases. */ $builds = array(); foreach($productBuilds as $key => $build) @@ -290,6 +305,17 @@ class buildModel extends model if((strpos($params, 'noterminate') !== false) and ($build->releaseStatus === 'terminate')) continue; $builds[$key] = ((strpos($params, 'withbranch') !== false and $build->branchName) ? $build->branchName . '/' : '') . $build->name; } + if($branch != 0) + { + $builds = $builds + $sysBuilds; + foreach($productBuildss as $key => $build) + { + if(empty($build->releaseID) and (strpos($params, 'nodone') !== false) and ($build->executionStatus === 'done')) continue; + if((strpos($params, 'noterminate') !== false) and ($build->releaseStatus === 'terminate')) continue; + $builds[$key] = ((strpos($params, 'withbranch') !== false and $build->branchName) ? $build->branchName . '/' : '') . $build->name; + } + } + if(!$builds) return $sysBuilds; @@ -305,7 +331,8 @@ class buildModel extends model foreach($releases as $buildID => $releaseName) $builds[$buildID] = ((strpos($params, 'withbranch') !== false and $productBuilds[$buildID]->branchName) ? $productBuilds[$buildID]->branchName . '/' : '') . $releaseName; } - return $sysBuilds + $builds; + if($branch != 0) return $builds; + else return $sysBuilds + $builds; } /** From b7e3fe90033d5e957c2b12e4b9fd91c40ddbe4ea Mon Sep 17 00:00:00 2001 From: zhouxin Date: Wed, 17 Nov 2021 15:49:24 +0800 Subject: [PATCH 07/42] Finis task #44486,44487,44488. --- module/action/lang/de.php | 7 +++ module/action/lang/en.php | 7 +++ module/action/lang/fr.php | 7 +++ module/action/lang/vi.php | 7 +++ module/action/lang/zh-cn.php | 8 +++ module/action/lang/zh-tw.php | 7 +++ module/action/model.php | 5 ++ module/common/model.php | 116 +++++++++++++++++++++++++++++++++++ module/execution/control.php | 13 ++++ module/execution/model.php | 1 + module/program/control.php | 2 + module/project/control.php | 1 + module/task/control.php | 21 +++++++ 13 files changed, 202 insertions(+) diff --git a/module/action/lang/de.php b/module/action/lang/de.php index 7d8d8eaca2..166a75bda5 100644 --- a/module/action/lang/de.php +++ b/module/action/lang/de.php @@ -150,6 +150,9 @@ $lang->action->desc->resolved = '$date, resolved by $actoraction->desc->managed = '$date, by $actor managed.' . "\n"; $lang->action->desc->estimated = '$date, by $actor estimated.' . "\n"; $lang->action->desc->run = '$date, by $actor executed.' . "\n"; +$lang->action->desc->syncprogram = '$date, started by $actor(The start of the project sets the status of the program as Ongoing)' . "\n"; +$lang->action->desc->syncproject = '$date, The start of the execution sets the status of the project as Ongoing' . "\n"; +$lang->action->desc->syncexecution = '$date, The start of the task sets the status of the execution as Ongoing' . "\n"; /* 子任务修改父任务的历史操作记录 */ $lang->action->desc->createchildren = '$date, $actor created a child task $extra。' . "\n"; @@ -265,6 +268,10 @@ $lang->action->label->reviewrejected = 'Reject'; $lang->action->label->reviewclarified = 'Clarify'; $lang->action->label->commitsummary = 'Commit Summary'; $lang->action->label->updatetrainee = 'Update Trainee'; +$lang->action->label->syncprogram = 'start'; +$lang->action->label->syncproject = 'start'; +$lang->action->label->syncexecution = 'start'; +$lang->action->label->startProgram = '(The start of the project sets the status of the program as Ongoing)'; /* 动态信息按照对象分组 */ $lang->action->dynamicAction = new stdclass; diff --git a/module/action/lang/en.php b/module/action/lang/en.php index 53a397e4d2..74bf4065d9 100755 --- a/module/action/lang/en.php +++ b/module/action/lang/en.php @@ -164,6 +164,9 @@ $lang->action->desc->resolved = '$date, resolved by $actoraction->desc->managed = '$date, by $actor managed.' . "\n"; $lang->action->desc->estimated = '$date, by $actor estimated.' . "\n"; $lang->action->desc->run = '$date, by $actor executed.' . "\n"; +$lang->action->desc->syncprogram = '$date, started by $actor(The start of the project sets the status of the program as Ongoing)' . "\n"; +$lang->action->desc->syncproject = '$date, The start of the execution sets the status of the project as Ongoing' . "\n"; +$lang->action->desc->syncexecution = '$date, The start of the task sets the status of the execution as Ongoing' . "\n"; /* Used to describe the history of operations related to parent-child tasks. */ $lang->action->desc->createchildren = '$date, $actor created a child task $extra。' . "\n"; @@ -280,6 +283,10 @@ $lang->action->label->reviewclarified = 'Clarify'; $lang->action->label->commitsummary = 'Commit Summary'; $lang->action->label->updatetrainee = 'Update Trainee'; $lang->action->label->setdefaultbranch = 'Set default branch'; +$lang->action->label->syncprogram = 'start'; +$lang->action->label->syncproject = 'start'; +$lang->action->label->syncexecution = 'start'; +$lang->action->label->startProgram = '(The start of the project sets the status of the program as Ongoing)'; /* Dynamic information is grouped by object. */ $lang->action->dynamicAction = new stdclass; diff --git a/module/action/lang/fr.php b/module/action/lang/fr.php index 0754fd1130..d54512e2ef 100644 --- a/module/action/lang/fr.php +++ b/module/action/lang/fr.php @@ -150,6 +150,9 @@ $lang->action->desc->resolved = '$date, resolved by $actoraction->desc->managed = '$date, by $actor managed.' . "\n"; $lang->action->desc->estimated = '$date, by $actor estimated.' . "\n"; $lang->action->desc->run = '$date, by $actor executed.' . "\n"; +$lang->action->desc->syncprogram = '$date, started by $actor(The start of the project sets the status of the program as Ongoing)' . "\n"; +$lang->action->desc->syncproject = '$date, The start of the execution sets the status of the project as Ongoing' . "\n"; +$lang->action->desc->syncexecution = '$date, The start of the task sets the status of the execution as Ongoing' . "\n"; /* 子任务修改父任务的历史操作记录 */ $lang->action->desc->createchildren = '$date, $actor a créé un sous-tâche $extra。' . "\n"; @@ -265,6 +268,10 @@ $lang->action->label->reviewrejected = 'Reject'; $lang->action->label->reviewclarified = 'Clarify'; $lang->action->label->commitsummary = 'Commit Summary'; $lang->action->label->updatetrainee = 'Update Trainee'; +$lang->action->label->syncprogram = 'start'; +$lang->action->label->syncproject = 'start'; +$lang->action->label->syncexecution = 'start'; +$lang->action->label->startProgram = '(The start of the project sets the status of the program as Ongoing)'; /* 动态信息按照对象分组 */ $lang->action->dynamicAction = new stdclass(); diff --git a/module/action/lang/vi.php b/module/action/lang/vi.php index 67a7ae7567..ce4be8b8ef 100644 --- a/module/action/lang/vi.php +++ b/module/action/lang/vi.php @@ -150,6 +150,9 @@ $lang->action->desc->resolved = '$date, resolved by $actoraction->desc->managed = '$date, by $actor managed.' . "\n"; $lang->action->desc->estimated = '$date, by $actor estimated.' . "\n"; $lang->action->desc->run = '$date, by $actor executed.' . "\n"; +$lang->action->desc->syncprogram = '$date, started by $actor(The start of the project sets the status of the program as Ongoing)' . "\n"; +$lang->action->desc->syncproject = '$date, The start of the execution sets the status of the project as Ongoing' . "\n"; +$lang->action->desc->syncexecution = '$date, The start of the task sets the status of the execution as Ongoing' . "\n"; /* Used to describe the history of operations related to parent-child tasks. */ $lang->action->desc->createchildren = '$date, $actor created a child task $extra。' . "\n"; @@ -265,6 +268,10 @@ $lang->action->label->reviewrejected = 'Reject'; $lang->action->label->reviewclarified = 'Clarify'; $lang->action->label->commitsummary = 'Commit Summary'; $lang->action->label->updatetrainee = 'Update Trainee'; +$lang->action->label->syncprogram = 'start'; +$lang->action->label->syncproject = 'start'; +$lang->action->label->syncexecution = 'start'; +$lang->action->label->startProgram = '(The start of the project sets the status of the program as Ongoing)'; /* Dynamic information is grouped by object. */ $lang->action->dynamicAction = new stdclass; diff --git a/module/action/lang/zh-cn.php b/module/action/lang/zh-cn.php index 7f71f341bc..8cbf105983 100755 --- a/module/action/lang/zh-cn.php +++ b/module/action/lang/zh-cn.php @@ -164,6 +164,10 @@ $lang->action->desc->resolved = '$date, 由 $actor 解 $lang->action->desc->managed = '$date, 由 $actor 维护。' . "\n"; $lang->action->desc->estimated = '$date, 由 $actor 估算。' . "\n"; $lang->action->desc->run = '$date, 由 $actor 执行。' . "\n"; +$lang->action->desc->syncprogram = '$date, 由 $actor 启动(因项目开始而启动项目集)。' . "\n"; +$lang->action->desc->syncproject = '$date, 系统判断由执行开始,将项目状态置为进行中。' . "\n"; +$lang->action->desc->syncexecution = '$date, 系统判断由任务开始,将执行状态置为进行中。' . "\n"; + /* 用来描述和父子任务相关的操作历史记录。*/ $lang->action->desc->createchildren = '$date, 由 $actor 创建子任务 $extra。' . "\n"; @@ -280,6 +284,10 @@ $lang->action->label->reviewclarified = '有待明确'; $lang->action->label->commitsummary = '提交培训总结'; $lang->action->label->updatetrainee = '更新培训人员'; $lang->action->label->setdefaultbranch = '设置了默认分支'; +$lang->action->label->syncprogram = '开始了'; +$lang->action->label->syncproject = '开始了'; +$lang->action->label->syncexecution = '开始了'; +$lang->action->label->startProgram = '(因项目开始而启动项目集)'; /* 动态信息按照对象分组 */ $lang->action->dynamicAction = new stdclass(); diff --git a/module/action/lang/zh-tw.php b/module/action/lang/zh-tw.php index 93f8d1a424..3cf12df3db 100755 --- a/module/action/lang/zh-tw.php +++ b/module/action/lang/zh-tw.php @@ -164,6 +164,9 @@ $lang->action->desc->resolved = '$date, 由 $actor 解 $lang->action->desc->managed = '$date, 由 $actor 維護。' . "\n"; $lang->action->desc->estimated = '$date, 由 $actor 估算。' . "\n"; $lang->action->desc->run = '$date, 由 $actor 執行。' . "\n"; +$lang->action->desc->syncprogram = '$date, 由 $actor 啟動(因項目開始而啟動項目集)。' . "\n"; +$lang->action->desc->syncproject = '$date, 系統判斷由執行開始,將項目狀態置為進行中。' . "\n"; +$lang->action->desc->syncexecution = '$date, 系統判斷由任務開始,將執行狀態置為進行中。' . "\n"; /* 用來描述和父子任務相關的操作歷史記錄。*/ $lang->action->desc->createchildren = '$date, 由 $actor 創建子任務 $extra。' . "\n"; @@ -279,6 +282,10 @@ $lang->action->label->reviewrejected = '拒絶'; $lang->action->label->reviewclarified = '有待明確'; $lang->action->label->commitsummary = '提交培訓總結'; $lang->action->label->updatetrainee = '更新培訓人員'; +$lang->action->label->syncprogram = '開始了'; +$lang->action->label->syncproject = '開始了'; +$lang->action->label->syncexecution = '開始了'; +$lang->action->label->startProgram = '(因項目開始而啟動項目集)'; /* 動態信息按照對象分組 */ $lang->action->dynamicAction = new stdclass(); diff --git a/module/action/model.php b/module/action/model.php index f5f9ad45e2..48edc495db 100755 --- a/module/action/model.php +++ b/module/action/model.php @@ -969,6 +969,11 @@ class actionModel extends model /* Add name field to the actions. */ $action->objectName = isset($objectNames[$action->objectType][$action->objectID]) ? $objectNames[$action->objectType][$action->objectID] : ''; + if($action->objectType =='program' and strpos('syncexecution,syncproject,syncprogram', $action->action) !==false) + { + $action->objectName .= $this->lang->action->label->startProgram; + } + $projectID = isset($relatedProjects[$action->objectType][$action->objectID]) ? $relatedProjects[$action->objectType][$action->objectID] : 0; $actionType = strtolower($action->action); diff --git a/module/common/model.php b/module/common/model.php index d33cbf4d0c..94e4c2c146 100644 --- a/module/common/model.php +++ b/module/common/model.php @@ -36,6 +36,122 @@ class commonModel extends model } } + /** + * Set the status of execution, project, and program to doing. + * + * @param int $objectID + * @access public + * @return void + */ + public function syncPPEStatus($objectID) + { + global $app; + $thisModule = $app->rawModule; + + if($this->config->systemMode == 'classic') + { + if($thisModule == 'task') $this->syncExecutionStatus($objectID); + } + + if($this->config->systemMode == 'new') + { + if($thisModule == 'task') + { + $taskID = $objectID; + $execution = $this->syncExecutionStatus($taskID); + $project = $this->syncProjectStatus($execution); + $this->syncProgramStatus($project); + } + if($thisModule == 'execution') + { + $executionID = $objectID; + $execution = $this->dao->select('*')->from(TABLE_EXECUTION)->where('id')->eq($executionID)->fetch(); + $project = $this->syncProjectStatus($execution); + $this->syncProgramStatus($project); + } + if($thisModule == 'project') + { + $projectID = $objectID; + $project = $this->dao->select('*')->from(TABLE_PROJECT)->where('id')->eq($projectID)->fetch(); + $this->syncProgramStatus($project); + } + if($thisModule == 'program') + { + $programID = $objectID; + $program = $this->dao->select('*')->from(TABLE_PROGRAM)->where('id')->eq($programID)->fetch(); + $this->syncProgramStatus($program); + } + } + } + + /** + * Set the status of the program to which theproject is linked as Ongoing. + * + * @param object $project + * @access public + * @return void + */ + public function syncProgramStatus($project) + { + if($project->parent == 0) return; + + $parentPath = str_replace(",{$project->id},", '', $project->path); + $parentPath = explode(',', trim($parentPath, ',')); + $waitList = $this->dao->select('id')->from(TABLE_PROGRAM) + ->where('id')->in($parentPath) + ->andWhere('status')->eq('wait') + ->orderBy('id_desc') + ->fetchPairs(); + + foreach($waitList as $programID) + { + $this->dao->update(TABLE_PROGRAM)->set('status')->eq('doing')->where('id')->eq($programID)->exec(); + $this->loadModel('action')->create('program', $programID, 'syncprogram'); + } + } + + /** + * Set the status of the project to which the execution is linked as Ongoing. + * + * @param object $execution + * @access public + * @return object $project + */ + public function syncProjectStatus($execution) + { + $projectID = $execution->project; + $project = $this->dao->select('*')->from(TABLE_PROJECT)->where('id')->eq($projectID)->fetch(); + + if($project->status == 'wait') + { + $this->dao->update(TABLE_PROJECT)->set('status')->eq('doing')->where('id')->eq($projectID)->exec(); + $this->loadModel('action')->create('project', $projectID, 'syncproject'); + } + return $project; + } + + /** + * Set the status of the execution to which the task is linked as Ongoing. + * + * @param int $taskID + * @access public + * @return object $execution + */ + public function syncExecutionStatus($taskID) + { + $execution = $this->dao->select('t1.*')->from(TABLE_EXECUTION)->alias('t1') + ->leftJoin(TABLE_TASK)->alias('t2')->on('t1.id=t2.execution') + ->where('t2.id')->eq($taskID) + ->fetch(); + + if($execution->status == 'wait') + { + $this->dao->update(TABLE_EXECUTION)->set('status')->eq('doing')->where('id')->eq($execution->id)->exec(); + $this->loadModel('action')->create('execution', $execution->id, 'syncexecution'); + } + return $execution; + } + /** * Set the header info. * diff --git a/module/execution/control.php b/module/execution/control.php index 6981d3f0de..58caaea281 100644 --- a/module/execution/control.php +++ b/module/execution/control.php @@ -1469,6 +1469,7 @@ class execution extends control } $this->executeHooks($executionID); + if($_POST['status'] == 'doing') $this->loadModel('common')->syncPPEStatus($executionID); return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => inlink('view', "executionID=$executionID"))); } @@ -1574,6 +1575,16 @@ class execution extends control $actionID = $this->loadModel('action')->create($this->objectType, $executionID, 'Edited'); $this->action->logHistory($actionID, $changes); + + foreach($changes as $changeField) + { + if($changeField['field'] == 'status' && $changeField['old'] =='wait' && $changeField['new'] =='doing') + { + $syncExecution = $executionID; + break; + } + } + $this->loadModel('common')->syncPPEStatus($syncExecution); } } die(js::locate($this->session->executionList, 'parent')); @@ -1651,6 +1662,8 @@ class execution extends control $actionID = $this->action->create($this->objectType, $executionID, 'Started', $this->post->comment); $this->action->logHistory($actionID, $changes); } + + $this->loadModel('common')->syncPPEStatus($executionID); $this->executeHooks($executionID); die(js::reload('parent.parent')); } diff --git a/module/execution/model.php b/module/execution/model.php index 7d8f0cdbbd..98fbb8e5bf 100644 --- a/module/execution/model.php +++ b/module/execution/model.php @@ -583,6 +583,7 @@ class executionModel extends model $oldExecutions = $this->getByIdList($this->post->executionIDList); $nameList = array(); $codeList = array(); + $projectModel = ''; /* Replace required language. */ if($this->app->tab == 'project') diff --git a/module/program/control.php b/module/program/control.php index b2ac179ed0..2c659df5ce 100644 --- a/module/program/control.php +++ b/module/program/control.php @@ -288,6 +288,8 @@ class program extends control $actionID = $this->action->create('program', $programID, 'Started', $this->post->comment); $this->action->logHistory($actionID, $changes); } + + $this->loadModel('common')->syncPPEStatus($programID); $this->executeHooks($programID); die(js::reload('parent.parent')); } diff --git a/module/project/control.php b/module/project/control.php index 86532a81f5..750d12f1b1 100644 --- a/module/project/control.php +++ b/module/project/control.php @@ -1421,6 +1421,7 @@ class project extends control } } } + $this->loadModel('common')->syncPPEStatus($projectID); $this->executeHooks($projectID); die(js::reload('parent.parent')); diff --git a/module/task/control.php b/module/task/control.php index 0c39718954..3cd0eb2f55 100644 --- a/module/task/control.php +++ b/module/task/control.php @@ -395,6 +395,8 @@ class task extends control $this->executeHooks($taskID); + if($_POST['status'] == 'doing') $this->loadModel('common')->syncPPEStatus($taskID); + if($task->fromBug != 0) { foreach($changes as $change) @@ -464,10 +466,25 @@ class task extends control if(!empty($allChanges)) { + /* updateStatus is a description of whether to update the responsibility performance*/ + $updateStatus = false; foreach($allChanges as $taskID => $changes) { if(empty($changes)) continue; + /* Determine whether the status of a task has been changed, if the status of a task has been changed, set $updateStatus to taskID*/ + if($updateStatus == false) + { + foreach($changes as $changeField) + { + if($changeField['field'] == 'status' && $changeField['new'] == 'doing') + { + $updateStatus = $taskID; + break; + } + } + } + $actionID = $this->loadModel('action')->create('task', $taskID, 'Edited'); $this->action->logHistory($actionID, $changes); @@ -484,6 +501,7 @@ class task extends control } } } + if($updateStatus !== false) $this->loadModel('common')->syncPPEStatus($updateStatus); } } $this->loadModel('score')->create('ajax', 'batchOther'); @@ -792,6 +810,7 @@ class task extends control } $this->executeHooks($taskID); + $this->loadModel('common')->syncPPEStatus($taskID); /* Remind whether to update status of the bug, if task which from that bug has been finished. */ if($changes and $this->task->needUpdateBugStatus($task)) @@ -838,6 +857,8 @@ class task extends control $changes = $this->task->recordEstimate($taskID); if(dao::isError()) die(js::error(dao::getError())); + $this->loadModel('common')->syncPPEStatus($taskID); + /* Remind whether to update status of the bug, if task which from that bug has been finished. */ $task = $this->task->getById($taskID); if($changes and $this->task->needUpdateBugStatus($task)) From 8cd05d8301616b4d7cf8a948c6a16de47c9fcb8f Mon Sep 17 00:00:00 2001 From: songchenxuan Date: Wed, 17 Nov 2021 16:22:43 +0800 Subject: [PATCH 08/42] * Finish task 44490. --- module/bug/control.php | 1 - module/bug/view/create.html.php | 1 - module/build/model.php | 10 +++++----- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/module/bug/control.php b/module/bug/control.php index 885fb495a1..422dff2193 100644 --- a/module/bug/control.php +++ b/module/bug/control.php @@ -207,7 +207,6 @@ class bug extends control $showModule = !empty($this->config->datatable->bugBrowse->showModule) ? $this->config->datatable->bugBrowse->showModule : ''; $productName = ($productID and isset($this->products[$productID])) ? $this->products[$productID] : $this->lang->product->allProduct; - $product = $this->product->getById($productID); if($product and $product->type != 'normal') { diff --git a/module/bug/view/create.html.php b/module/bug/view/create.html.php index 066be43156..2bbe2f801c 100644 --- a/module/bug/view/create.html.php +++ b/module/bug/view/create.html.php @@ -120,7 +120,6 @@ js::set('moduleID', $moduleID);
bug->openedBuild?> -
bug->allBuilds, "class='btn' id='all' data-toggle='tooltip' onclick='loadAllBuilds()'")?>
diff --git a/module/build/model.php b/module/build/model.php index fea109c4dd..a93e8b1334 100644 --- a/module/build/model.php +++ b/module/build/model.php @@ -269,6 +269,7 @@ class buildModel extends model */ public function getProductBuildPairs($products, $branch = 0, $params = 'noterminate, nodone', $replace = true) { + $branch = str_replace('0,', '', $branch); if($branch == 'all') $branch = 0; $sysBuilds = array(); if(strpos($params, 'noempty') === false) $sysBuilds = array('' => ''); @@ -283,10 +284,10 @@ class buildModel extends model ->andWhere('t1.deleted')->eq(0) ->orderBy('t1.date desc, t1.id desc')->fetchAll('id'); - $productBuildss = array(); if($branch != 0) { - $productBuildss = $this->dao->select('t1.id, t1.name, t1.execution, t2.status as executionStatus, t3.id as releaseID, t3.status as releaseStatus, t4.name as branchName')->from(TABLE_BUILD)->alias('t1') + $mainProductBuilds = array(); + $mainProductBuilds = $this->dao->select('t1.id, t1.name, t1.execution, t2.status as executionStatus, t3.id as releaseID, t3.status as releaseStatus, t4.name as branchName')->from(TABLE_BUILD)->alias('t1') ->leftJoin(TABLE_EXECUTION)->alias('t2')->on('t1.execution = t2.id') ->leftJoin(TABLE_RELEASE)->alias('t3')->on('t1.id = t3.build') ->leftJoin(TABLE_BRANCH)->alias('t4')->on('t1.branch = t4.id') @@ -308,7 +309,7 @@ class buildModel extends model if($branch != 0) { $builds = $builds + $sysBuilds; - foreach($productBuildss as $key => $build) + foreach($mainProductBuilds as $key => $build) { if(empty($build->releaseID) and (strpos($params, 'nodone') !== false) and ($build->executionStatus === 'done')) continue; if((strpos($params, 'noterminate') !== false) and ($build->releaseStatus === 'terminate')) continue; @@ -331,8 +332,7 @@ class buildModel extends model foreach($releases as $buildID => $releaseName) $builds[$buildID] = ((strpos($params, 'withbranch') !== false and $productBuilds[$buildID]->branchName) ? $productBuilds[$buildID]->branchName . '/' : '') . $releaseName; } - if($branch != 0) return $builds; - else return $sysBuilds + $builds; + return $branch ? $builds : $sysBuilds + $builds; } /** From 27d509df08752bed87c46d11583b88f8a74e188c Mon Sep 17 00:00:00 2001 From: tianshujie Date: Wed, 17 Nov 2021 16:28:30 +0800 Subject: [PATCH 09/42] * Finish task #44496. --- module/testcase/js/batchedit.js | 28 ++++++++++++++++++++++++- module/testcase/js/common.js | 12 ++++++----- module/testcase/model.php | 8 +++---- module/testcase/view/batchedit.html.php | 5 +++-- module/testcase/view/edit.html.php | 1 + 5 files changed, 42 insertions(+), 12 deletions(-) diff --git a/module/testcase/js/batchedit.js b/module/testcase/js/batchedit.js index 618c579d39..a9cc368d19 100644 --- a/module/testcase/js/batchedit.js +++ b/module/testcase/js/batchedit.js @@ -31,13 +31,39 @@ function loadBranches(product, branch, caseID) if(!branch) branch = 0; moduleLink = createLink('tree', 'ajaxGetOptionMenu', 'productID=' + product + '&viewtype=case&branch=' + branch + '&rootModuleID=0&returnType=html&fieldID=' + caseID + '&needManage=true'); - $('#modules' + caseID).parent('td').load(moduleLink, function(){$('#modules' + caseID).chosen();}) + $('#modules' + caseID).parent('td').load(moduleLink, function() + { + $("#modules" + caseID).attr('onchange', "loadStories("+ product + ", this.value, " + caseID + ")").chosen(); + }); + + loadStories(product, 0, caseID); } $(function() { removeDitto(); //Remove 'ditto' in first row. $('#subNavbar li[data-id="testcase"]').addClass('active'); + if($("[name^='story']").length > 0) + { + $("[name^='story']").each(function() + { + var id = $(this).attr('id'); + var num = id.substring(5); + var moduleID = $('#modules' + num).val(); + var branchID = $('#branches' + num).val(); + var storyID = $("#story" + num).val(); + var storyLink = createLink('story', 'ajaxGetProductStories', 'productID=' + productID + '&branch=' + branchID + '&moduleID=' + moduleID + '&storyID=0&onlyOption=false&status=noclosed&limit=50&type=full&hasParent=1&executionID=0&number=' + num); + $.get(storyLink, function(stories) + { + if(!stories) modules = ''; + $('#story' + num).replaceWith(stories); + $('#story' + num + "_chosen").remove(); + $('#story' + num).next('.picker').remove(); + $('#story' + num).attr('name', 'story[' + num + ']').chosen(); + $('#story' + num).val(storyID).trigger('chosen:updated'); + }); + }); + } }); $(document).on('click', '.chosen-with-drop', function(){oldValue = $(this).prev('select').val();})//Save old value. diff --git a/module/testcase/js/common.js b/module/testcase/js/common.js index 38e2ae74ce..8ec70ffa3f 100644 --- a/module/testcase/js/common.js +++ b/module/testcase/js/common.js @@ -355,14 +355,16 @@ function updateStepID() */ function loadStories(productID, moduleID, num) { - var branchID = $('#branch' + num).val(); - var storyLink = createLink('story', 'ajaxGetProductStories', 'productID=' + productID + '&branch=' + branchID + '&moduleID=' + moduleID + '&storyID=0&onlyOption=false&status=noclosed&limit=50&type=full&hasParent=1&executionID=0&number=' + num); + var branchIDName = config.currentMethod == 'batchcreate' ? '#branch' : '#branches'; + var branchID = $(branchIDName + num).val(); + var storyLink = createLink('story', 'ajaxGetProductStories', 'productID=' + productID + '&branch=' + branchID + '&moduleID=' + moduleID + '&storyID=0&onlyOption=false&status=noclosed&limit=50&type=full&hasParent=1&executionID=0&number=' + num); $.get(storyLink, function(stories) { if(!stories) modules = ''; $('#story' + num).replaceWith(stories); - $("#story" + num + "_chosen").remove(); - $("#story" + num).next('.picker').remove(); - $("#story" + num).chosen(); + $('#story' + num + "_chosen").remove(); + $('#story' + num).next('.picker').remove(); + $('#story' + num).attr('name', 'story[' + num + ']'); + $('#story' + num).chosen(); }); } diff --git a/module/testcase/model.php b/module/testcase/model.php index ad374b9e53..c53bcbd59c 100644 --- a/module/testcase/model.php +++ b/module/testcase/model.php @@ -863,13 +863,13 @@ class testcaseModel extends model if($data->pris[$caseID] == 'ditto') $data->pris[$caseID] = isset($prev['pri']) ? $prev['pri'] : 3; if($data->branches[$caseID] == 'ditto') $data->branches[$caseID] = isset($prev['branch']) ? $prev['branch'] : 0; if($data->modules[$caseID] == 'ditto') $data->modules[$caseID] = isset($prev['module']) ? $prev['module'] : 0; - if($data->stories[$caseID] == 'ditto') $data->stories[$caseID] = isset($prev['story']) ? $prev['story'] : 0; + if($data->story[$caseID] == 'ditto') $data->story[$caseID] = isset($prev['story']) ? $prev['story'] : 0; if($data->types[$caseID] == 'ditto') $data->types[$caseID] = isset($prev['type']) ? $prev['type'] : ''; - if($data->stories[$caseID] == '') $data->stories[$caseID] = 0; + if($data->story[$caseID] == '') $data->story[$caseID] = 0; $prev['pri'] = $data->pris[$caseID]; $prev['type'] = $data->types[$caseID]; - $prev['story'] = $data->stories[$caseID]; + $prev['story'] = $data->story[$caseID]; $prev['branch'] = $data->branches[$caseID]; $prev['module'] = $data->modules[$caseID]; } @@ -885,7 +885,7 @@ class testcaseModel extends model $case->branch = $data->branches[$caseID]; $case->module = $data->modules[$caseID]; $case->status = $data->statuses[$caseID]; - $case->story = $data->stories[$caseID]; + $case->story = $data->story[$caseID]; $case->color = $data->color[$caseID]; $case->title = $data->title[$caseID]; $case->precondition = $data->precondition[$caseID]; diff --git a/module/testcase/view/batchedit.html.php b/module/testcase/view/batchedit.html.php index 88762552cf..bc98616b26 100644 --- a/module/testcase/view/batchedit.html.php +++ b/module/testcase/view/batchedit.html.php @@ -12,6 +12,7 @@ ?> lang->testcase->dittoNotice);?> +

testcase->common . $lang->colon . $lang->testcase->batchEdit;?>

@@ -103,8 +104,8 @@ branch, "class='form-control chosen' onchange='loadBranches($branchProductID, this.value, $caseID)', $disabled");?> - ' style='overflow:visible'>module, "class='form-control chosen'");?> - ' style='overflow:visible'>story, "class='form-control chosen'");?> + ' style='overflow:visible'>module, "class='form-control chosen' onchange='loadStories($productID, this.value, $caseID)'");?> + ' style='overflow:visible'>story, "class='form-control chosen'");?>
diff --git a/module/testcase/view/edit.html.php b/module/testcase/view/edit.html.php index 46cebce8aa..9764da5f59 100644 --- a/module/testcase/view/edit.html.php +++ b/module/testcase/view/edit.html.php @@ -16,6 +16,7 @@ testcase->insertBefore);?> testcase->insertAfter);?> id);?> +execution);?>

From 5b00c80c4c003647dafd9e8c32181007fcaa0229 Mon Sep 17 00:00:00 2001 From: songchenxuan Date: Wed, 17 Nov 2021 17:19:29 +0800 Subject: [PATCH 10/42] * Finish task 44490 --- module/build/model.php | 28 ++-------------------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/module/build/model.php b/module/build/model.php index a93e8b1334..5be6019502 100644 --- a/module/build/model.php +++ b/module/build/model.php @@ -280,24 +280,10 @@ class buildModel extends model ->leftJoin(TABLE_RELEASE)->alias('t3')->on('t1.id = t3.build') ->leftJoin(TABLE_BRANCH)->alias('t4')->on('t1.branch = t4.id') ->where('t1.product')->in($products) - ->beginIF($branch)->andWhere('t1.branch')->in("$branch")->fi() + ->andWhere('t1.branch')->eq($branch)->fi() ->andWhere('t1.deleted')->eq(0) ->orderBy('t1.date desc, t1.id desc')->fetchAll('id'); - if($branch != 0) - { - $mainProductBuilds = array(); - $mainProductBuilds = $this->dao->select('t1.id, t1.name, t1.execution, t2.status as executionStatus, t3.id as releaseID, t3.status as releaseStatus, t4.name as branchName')->from(TABLE_BUILD)->alias('t1') - ->leftJoin(TABLE_EXECUTION)->alias('t2')->on('t1.execution = t2.id') - ->leftJoin(TABLE_RELEASE)->alias('t3')->on('t1.id = t3.build') - ->leftJoin(TABLE_BRANCH)->alias('t4')->on('t1.branch = t4.id') - ->where('t1.product')->in($products) - ->beginIF($branch)->andWhere('t1.branch')->in("0")->fi() - ->andWhere('t1.deleted')->eq(0) - ->orderBy('t1.date desc, t1.id desc')->fetchAll('id'); - } - - /* Set builds and filter done executions and terminate releases. */ $builds = array(); foreach($productBuilds as $key => $build) @@ -306,16 +292,6 @@ class buildModel extends model if((strpos($params, 'noterminate') !== false) and ($build->releaseStatus === 'terminate')) continue; $builds[$key] = ((strpos($params, 'withbranch') !== false and $build->branchName) ? $build->branchName . '/' : '') . $build->name; } - if($branch != 0) - { - $builds = $builds + $sysBuilds; - foreach($mainProductBuilds as $key => $build) - { - if(empty($build->releaseID) and (strpos($params, 'nodone') !== false) and ($build->executionStatus === 'done')) continue; - if((strpos($params, 'noterminate') !== false) and ($build->releaseStatus === 'terminate')) continue; - $builds[$key] = ((strpos($params, 'withbranch') !== false and $build->branchName) ? $build->branchName . '/' : '') . $build->name; - } - } if(!$builds) return $sysBuilds; @@ -332,7 +308,7 @@ class buildModel extends model foreach($releases as $buildID => $releaseName) $builds[$buildID] = ((strpos($params, 'withbranch') !== false and $productBuilds[$buildID]->branchName) ? $productBuilds[$buildID]->branchName . '/' : '') . $releaseName; } - return $branch ? $builds : $sysBuilds + $builds; + return $sysBuilds + $builds; } /** From 79fa0758545ab313b2a65c1fe7133049fd8a75c4 Mon Sep 17 00:00:00 2001 From: tianshujie Date: Wed, 17 Nov 2021 17:25:59 +0800 Subject: [PATCH 11/42] * Scope of repair stories. --- module/testcase/control.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/testcase/control.php b/module/testcase/control.php index 73501f4046..bd5634bc20 100644 --- a/module/testcase/control.php +++ b/module/testcase/control.php @@ -523,7 +523,7 @@ class testcase extends control /* Set story list. */ $story = $storyID ? $this->story->getByID($storyID) : ''; - $storyList = $this->loadModel('story')->getProductStoryPairs($productID, $branch); + $storyList = $this->loadModel('story')->getProductStoryPairs($productID, $branch === 'all' ? 0 : $branch); $storyList += $storyID ? array($storyID => $story->id . ':' . $story->title) : array(''); /* Set module option menu. */ From 328527a5bdaa1369c5c513330ecb955543cb17f5 Mon Sep 17 00:00:00 2001 From: songchenxuan Date: Wed, 17 Nov 2021 17:28:28 +0800 Subject: [PATCH 12/42] * Finish task 44490 --- module/build/model.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/module/build/model.php b/module/build/model.php index 5be6019502..3b19816faf 100644 --- a/module/build/model.php +++ b/module/build/model.php @@ -227,6 +227,8 @@ class buildModel extends model */ public function getExecutionBuildPairs($executionID, $productID, $branch = 0, $params = '', $buildIdList = '') { + $branch = str_replace('0,', '', $branch); + if($branch == 'all') $branch = 0; $sysBuilds = array(); $selectedBuilds = array(); if(strpos($params, 'noempty') === false) $sysBuilds = array('' => ''); @@ -239,7 +241,7 @@ class buildModel extends model ->leftJoin(TABLE_BRANCH)->alias('t4')->on('t1.branch = t4.id') ->where('t1.execution')->eq((int)$executionID) ->beginIF($productID)->andWhere('t1.product')->eq((int)$productID)->fi() - ->beginIF($branch)->andWhere('t1.branch')->in("0,$branch")->fi() + ->andWhere('t1.branch')->in($branch)->fi() ->andWhere('t1.deleted')->eq(0) ->orderBy('t1.date desc, t1.id desc')->fetchAll('id'); From 59f3ac666153ef31bda472de5b0c1c4594bdba8a Mon Sep 17 00:00:00 2001 From: songchenxuan Date: Wed, 17 Nov 2021 17:42:10 +0800 Subject: [PATCH 13/42] * Finish task 44490 --- module/bug/control.php | 1 + 1 file changed, 1 insertion(+) diff --git a/module/bug/control.php b/module/bug/control.php index 422dff2193..885fb495a1 100644 --- a/module/bug/control.php +++ b/module/bug/control.php @@ -207,6 +207,7 @@ class bug extends control $showModule = !empty($this->config->datatable->bugBrowse->showModule) ? $this->config->datatable->bugBrowse->showModule : ''; $productName = ($productID and isset($this->products[$productID])) ? $this->products[$productID] : $this->lang->product->allProduct; + $product = $this->product->getById($productID); if($product and $product->type != 'normal') { From c75f8941e5b1a3e29cf9a775c896996d12b7258d Mon Sep 17 00:00:00 2001 From: songchenxuan Date: Wed, 17 Nov 2021 17:44:10 +0800 Subject: [PATCH 14/42] * Finish task 44490 --- module/build/model.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/build/model.php b/module/build/model.php index 3b19816faf..65b97d3048 100644 --- a/module/build/model.php +++ b/module/build/model.php @@ -241,7 +241,7 @@ class buildModel extends model ->leftJoin(TABLE_BRANCH)->alias('t4')->on('t1.branch = t4.id') ->where('t1.execution')->eq((int)$executionID) ->beginIF($productID)->andWhere('t1.product')->eq((int)$productID)->fi() - ->andWhere('t1.branch')->in($branch)->fi() + ->andWhere('t1.branch')->eq($branch)->fi() ->andWhere('t1.deleted')->eq(0) ->orderBy('t1.date desc, t1.id desc')->fetchAll('id'); From 47cdcd78c0644641507520af0c4d7014d1e19284 Mon Sep 17 00:00:00 2001 From: xieqiyu Date: Wed, 17 Nov 2021 19:30:13 +0800 Subject: [PATCH 15/42] * Code for task#44497. --- module/bug/control.php | 4 +- module/build/control.php | 4 +- module/build/js/common.js | 2 +- module/build/view/create.html.php | 2 +- module/execution/control.php | 49 +++++++++--------- module/execution/model.php | 42 ++++------------ module/execution/view/view.html.php | 12 +++-- module/product/control.php | 4 +- module/product/model.php | 77 ++++++++++++++--------------- module/project/control.php | 33 ++++++++----- module/project/model.php | 28 +---------- module/project/view/create.html.php | 2 +- module/project/view/view.html.php | 4 +- module/story/model.php | 18 +++---- module/testcase/control.php | 6 +-- 15 files changed, 124 insertions(+), 163 deletions(-) diff --git a/module/bug/control.php b/module/bug/control.php index 3052e26bef..bc0ff219eb 100644 --- a/module/bug/control.php +++ b/module/bug/control.php @@ -55,12 +55,12 @@ class bug extends control if($this->app->tab == 'project') { $objectID = $this->session->project; - $products = $this->loadModel('project')->getProducts($objectID, false); + $products = $this->product->getProducts($objectID, 'all', '', false); } elseif($this->app->tab == 'execution') { $objectID = $this->session->execution; - $products = $this->loadModel('execution')->getProducts($objectID, false); + $products = $this->product->getProducts($objectID, 'all', '', false); } else { diff --git a/module/build/control.php b/module/build/control.php index 292221e7f0..d93836dafd 100644 --- a/module/build/control.php +++ b/module/build/control.php @@ -63,7 +63,7 @@ class build extends control $executions = $this->execution->getPairs($execution->project); } - $productGroups = $this->execution->getProducts($executionID); + $productGroups = $this->loadModel('product')->getProducts($executionID); $productID = $productID ? $productID : key($productGroups); $branchGroups = $this->loadModel('project')->getBranchesByProject($executionID); $branchPairs = $this->loadModel('branch')->getPairs($productID, 'active'); @@ -156,7 +156,7 @@ class build extends control $executions = $this->product->getExecutionPairsByProduct($build->product, $build->branch, 'id_desc', $this->session->project, 'stagefilter'); if(!isset($executions[$build->execution])) $executions[$build->execution] = $execution->name; - $productGroups = $this->execution->getProducts($build->execution); + $productGroups = $this->product->getProducts($build->execution); if(!isset($productGroups[$build->product])) { diff --git a/module/build/js/common.js b/module/build/js/common.js index 13f263f902..0726ba7db2 100644 --- a/module/build/js/common.js +++ b/module/build/js/common.js @@ -12,7 +12,7 @@ function loadBranches(productID) var oldBranch = 0; if(typeof(productGroups[productID]) != "undefined") { - oldBranch = productGroups[productID]['branch']; + oldBranch = productGroups[productID]['branches']; } $.get(createLink('branch', 'ajaxGetBranches', 'productID=' + productID + '&oldBranch=0¶m=&projectID=' + executionID), function(data) diff --git a/module/build/view/create.html.php b/module/build/view/create.html.php index 0c1d6bb3cf..e898e67a29 100644 --- a/module/build/view/create.html.php +++ b/module/build/view/create.html.php @@ -35,7 +35,7 @@ if(!empty($product) and $product->type != 'normal' and isset($branches[$product->id])) { $branches = $branches[$product->id]; - echo "" . html::select('branch', $branches, $product->branch, "class='form-control chosen'"); + echo "" . html::select('branch', $branches, key($product->branches), "class='form-control chosen'"); } ?>

diff --git a/module/execution/control.php b/module/execution/control.php index 6981d3f0de..323bfc4854 100644 --- a/module/execution/control.php +++ b/module/execution/control.php @@ -99,7 +99,7 @@ class execution extends control /* Get executions and products info. */ $executionID = $this->execution->saveState($executionID, $this->executions); $execution = $this->execution->getById($executionID); - $products = $this->execution->getProducts($executionID); + $products = $this->product->getProducts($executionID); $childExecutions = $this->execution->getChildExecutions($executionID); $teamMembers = $this->execution->getTeamMembers($executionID); $actions = $this->loadModel('action')->getList($this->objectType, $executionID); @@ -746,7 +746,7 @@ class execution extends control /* Build the search form. */ $modules = array(); $executionModules = $this->loadModel('tree')->getTaskTreeModules($executionID, true); - $products = $this->execution->getProducts($executionID); + $products = $this->product->getProducts($executionID); foreach($products as $product) { $productModules = $this->tree->getOptionMenu($product->id); @@ -864,8 +864,8 @@ class execution extends control $queryID = ($type == 'bysearch') ? (int)$param : 0; $execution = $this->commonAction($executionID); $executionID = $execution->id; - $products = $this->execution->getProducts($execution->id); - $branchID = isset($products[$productID]) ? $products[$productID]->branch : 0; + $products = $this->product->getProducts($execution->id); + $branchID = isset($products[$productID]) ? current($products[$productID]->branches) : 0; $productPairs = array('0' => $this->lang->product->all); foreach($products as $product) $productPairs[$product->id] = $product->name; @@ -936,7 +936,7 @@ class execution extends control $this->session->set('caseList', $uri, 'execution'); $this->session->set('bugList', $uri, 'execution'); - $products = $this->execution->getProducts($executionID); + $products = $this->product->getProducts($executionID); $productID = key($products); // Get the first product for creating testcase. /* Load pager. */ @@ -999,7 +999,7 @@ class execution extends control $executionID = $execution->id; /* Get products' list. */ - $products = $this->execution->getProducts($executionID, false); + $products = $this->product->getProducts($executionID, 'all', '', false); $products = array('' => '') + $products; /* Build the search form. */ @@ -1020,7 +1020,7 @@ class execution extends control /* Set execution builds. */ $executionBuilds = array(); - $productList = $this->execution->getProducts($executionID); + $productList = $this->product->getProducts($executionID); if(!empty($builds)) { foreach($builds as $build) @@ -1282,7 +1282,7 @@ class execution extends control $acl = $copyExecution->acl; $whitelist = $copyExecution->whitelist; $projectID = $copyExecution->project; - $products = $this->execution->getProducts($copyExecutionID); + $products = $this->loadModel('product')->getProducts($copyExecutionID); $branches = $this->project->getBranchesByProject($copyExecutionID); $plans = $this->loadModel('productplan')->getGroupByProduct(array_keys($products)); $branchGroups = $this->execution->getBranchByProduct(array_keys($products), $projectID); @@ -1357,7 +1357,8 @@ class execution extends control { $this->lang->execution->type = str_replace($this->lang->executionCommon, $this->lang->project->stage, $this->lang->execution->type); } - $this->loadModel('user'); + + $this->loadModel('user'); $poUsers = $this->user->getPairs('noclosed|nodeleted|pofirst', '', $this->config->maxCount); if(!empty($this->config->user->moreLink)) $this->config->moreLinks["PM"] = $this->config->user->moreLink; @@ -1415,6 +1416,7 @@ class execution extends control public function edit($executionID, $action = 'edit', $extra = '') { /* Load language files and get browseExecutionLink. */ + $this->loadModel('product'); $this->app->loadLang('program'); $this->app->loadLang('stage'); $this->app->loadLang('programplan'); @@ -1423,7 +1425,7 @@ class execution extends control if(!empty($_POST)) { $oldPlans = $this->dao->select('plan')->from(TABLE_PROJECTPRODUCT)->where('project')->eq($executionID)->andWhere('plan')->ne(0)->fetchPairs('plan'); - $oldProducts = $this->execution->getProducts($executionID); + $oldProducts = $this->product->getProducts($executionID); $changes = $this->execution->update($executionID); if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); @@ -1438,7 +1440,7 @@ class execution extends control } $oldProducts = array_keys($oldProducts); - $newProducts = $this->execution->getProducts($executionID); + $newProducts = $this->product->getProducts($executionID); $newProducts = array_keys($newProducts); $diffProducts = array_merge(array_diff($oldProducts, $newProducts), array_diff($newProducts, $oldProducts)); $products = $diffProducts ? join(',', $newProducts) : ''; @@ -1486,14 +1488,12 @@ class execution extends control $position[] = html::a($browseExecutionLink, $execution->name); $position[] = $this->lang->execution->edit; - $allProducts = array(0 => ''); - $executionProsucts = $this->execution->getProducts($execution->project, true, 'noclosed'); - foreach($executionProsucts as $product) $allProducts[$product->id] = $product->name; + $allProducts = array(0 => '') + $this->product->getProducts($execution->project, 'noclosed', '', false); $this->loadModel('productplan'); $productPlans = array(0 => ''); $linkedBranches = array(); - $linkedProducts = $this->execution->getProducts($executionID); + $linkedProducts = $this->product->getProducts($executionID); $branches = $this->project->getBranchesByProject($executionID); $plans = $this->productplan->getGroupByProduct(array_keys($linkedProducts)); $executionStories = $this->project->getStoriesByProject($executionID); @@ -1546,7 +1546,7 @@ class execution extends control $this->view->branches = $branches; $this->view->unmodifiableProducts = $unmodifiableProducts; $this->view->unmodifiableBranches = $unmodifiableBranches; - $this->view->multiBranchProducts = $this->loadModel('product')->getMultiBranchPairs(); + $this->view->multiBranchProducts = $this->product->getMultiBranchPairs(); $this->view->productPlans = $productPlans; $this->view->branchGroups = $this->execution->getBranchByProduct(array_keys($linkedProducts), $execution->project); $this->display(); @@ -1828,11 +1828,14 @@ class execution extends control /* Execution not found to prevent searching for .*/ if(!isset($this->executions[$execution->id])) $this->executions = $this->execution->getPairs($execution->project, 'all', 'nocode'); - $products = $this->execution->getProducts($execution->id); + $products = $this->loadModel('product')->getProducts($execution->id); $linkedBranches = array(); foreach($products as $product) { - if($product->branch) $linkedBranches[$product->branch] = $product->branch; + if(isset($product->branches)) + { + foreach($product->branches as $branchID) $linkedBranches[$branchID] = $branchID; + } } /* Set menu. */ @@ -2245,7 +2248,7 @@ class execution extends control if(count($_POST['products']) > 1) die(js::alert($this->lang->execution->oneProduct) . js::locate($this->createLink('execution', 'manageProducts', "executionID=$executionID&from=$from"))); } - $oldProducts = $this->execution->getProducts($executionID); + $oldProducts = $this->product->getProducts($executionID); if($from == 'buildCreate' && $this->session->buildCreate) $browseExecutionLink = $this->session->buildCreate; @@ -2253,7 +2256,7 @@ class execution extends control if(dao::isError()) die(js::error(dao::getError())); $oldProducts = array_keys($oldProducts); - $newProducts = $this->execution->getProducts($executionID); + $newProducts = $this->product->getProducts($executionID); $newProducts = array_keys($newProducts); $diffProducts = array_merge(array_diff($oldProducts, $newProducts), array_diff($newProducts, $oldProducts)); if($diffProducts) $this->loadModel('action')->create($this->objectType, $executionID, 'Managed', '', !empty($_POST['products']) ? join(',', $_POST['products']) : ''); @@ -2274,7 +2277,7 @@ class execution extends control $position[] = $this->lang->execution->manageProducts; $allProducts = $this->config->systemMode == 'classic' ? $this->product->getPairs('noclosed') : $this->product->getProductPairsByProject($execution->project); - $linkedProducts = $this->execution->getProducts($execution->id); + $linkedProducts = $this->product->getProducts($execution->id); $linkedBranches = array(); $branches = $this->project->getBranchesByProject($executionID); $executionStories = $this->project->getStoriesByProject($executionID); @@ -2431,7 +2434,7 @@ class execution extends control /* Get projects, executions and products. */ $object = $this->project->getByID($objectID, $this->app->tab == 'project' ? 'project' : 'sprint,stage'); - $products = $this->project->getProducts($objectID); + $products = $this->product->getProducts($objectID); $browseLink = $this->createLink($this->app->tab == 'project' ? 'projectstory' : 'execution', 'story', "objectID=$objectID"); $this->session->set('storyList', $this->app->getURI(true), $this->app->tab); // Save session. @@ -2681,7 +2684,7 @@ class execution extends control */ public function ajaxGetProducts($executionID) { - $products = $this->execution->getProducts($executionID, false); + $products = $this->loadModel('product')->getProducts($executionID, 'all', '', false); die(html::select('product', $products, '', 'class="form-control"')); } diff --git a/module/execution/model.php b/module/execution/model.php index 3668ac5b05..da342cd500 100644 --- a/module/execution/model.php +++ b/module/execution/model.php @@ -544,7 +544,7 @@ class executionModel extends model $changedAccounts[$owner] = $owner; $teamMembers[$ownerField] = $member; } - if($execution->project) $this->addProjectMembers($execution->project, $teamMembers); + if($execution->project) $this->addProjectMembers($execution->project, $teamMembers); $whitelist = explode(',', $execution->whitelist); $this->loadModel('personnel')->updateWhitelist($whitelist, 'sprint', $executionID); @@ -1628,33 +1628,6 @@ class executionModel extends model return $managers; } - /** - * Get products of a execution. - * - * @param int $executionID - * @param bool $withBranch - * @access public - * @return array - */ - public function getProducts($executionID, $withBranch = true, $status = 'all') - { - if(defined('TUTORIAL')) - { - if(!$withBranch) return $this->loadModel('tutorial')->getProductPairs(); - return $this->loadModel('tutorial')->getExecutionProducts(); - } - - $query = $this->dao->select('t2.id, t2.name, t2.type, t1.branch, t1.plan')->from(TABLE_PROJECTPRODUCT)->alias('t1') - ->leftJoin(TABLE_PRODUCT)->alias('t2') - ->on('t1.product = t2.id') - ->where('t1.project')->eq((int)$executionID) - ->andWhere('t2.deleted')->eq(0) - ->beginIF(strpos($status, 'noclosed') !== false)->andWhere('status')->ne('closed')->fi() - ->beginIF(!$this->app->user->admin)->andWhere('t2.id')->in($this->app->user->view->products)->fi(); - if(!$withBranch) return $query->fetchPairs('id', 'name'); - return $query->fetchAll('id'); - } - /** * Get branch pairs by product id list. * @@ -3108,7 +3081,7 @@ class executionModel extends model } } - $branchGroups = $this->loadModel('branch')->getByProducts(array_keys($products), 'noempty'); + $branchGroups = $this->loadModel('branch')->getByProducts(array_keys($products)); $branchPairs = array(); $productType = 'normal'; $productNum = count($products); @@ -3119,9 +3092,12 @@ class executionModel extends model if($product->type != 'normal') { $productType = $product->type; - if($product->branch and isset($branchGroups[$product->id][$product->branch])) + if(isset($product->branches)) { - $branchPairs[$product->branch] = (count($products) > 1 ? $product->name . '/' : '') . $branchGroups[$product->id][$product->branch]; + foreach($product->branches as $branch) + { + if(isset($branchGroups[$product->id][$branch])) $branchPairs[$branch] = (count($products) > 1 ? $product->name . '/' : '') . $branchGroups[$product->id][$branch]; + } } else { @@ -3681,7 +3657,7 @@ class executionModel extends model $productPlans = array(); foreach($products as $productID => $product) { - foreach($product->branch as $branchID) + foreach($product->branches as $branchID) { $planInfo = $this->productplan->getPairs($product->id, $branchID); foreach($planInfo as $planID => $plan) @@ -3760,7 +3736,7 @@ class executionModel extends model { $this->loadModel('user')->updateUserView($executionID, $objectType, $users); - $products = $this->getProducts($executionID, $withBranch = false); + $products = $this->loadModel('product')->getProducts($executionID, 'all', '', false); if(!empty($products)) $this->user->updateUserView(array_keys($products), 'product', $users); } diff --git a/module/execution/view/view.html.php b/module/execution/view/view.html.php index d9ff7abf36..9a0554305c 100644 --- a/module/execution/view/view.html.php +++ b/module/execution/view/view.html.php @@ -200,11 +200,13 @@
$product):?> - branch]) ? '/' . $branchGroups[$productID][$product->branch] : '';?> + branches as $branchID):?> +
- createLink('product', 'browse', "productID=$productID&branch=$product->branch"), " " . $product->name . $branchName);?> + createLink('product', 'browse', "productID=$productID&branch=$branchID"), " " . $product->name . $branchName);?>
+
@@ -213,10 +215,12 @@
$product):?> - plan])):?> -
createLink('productplan', 'view', "planID={$product->plan}"), $product->name . '/' . $planGroups[$productID][$product->plan]);?>
+ plans as $planID):?> + +
createLink('productplan', 'view', "planID={$planID}"), $product->name . '/' . $planGroups[$productID][$planID]);?>
+
diff --git a/module/product/control.php b/module/product/control.php index 1e53c206c3..66adb9ca21 100644 --- a/module/product/control.php +++ b/module/product/control.php @@ -216,12 +216,12 @@ class product extends control $this->app->loadLang('datatable'); $this->lang->datatable->showBranch = sprintf($this->lang->datatable->showBranch, $this->lang->product->branchName[$product->type]); } - + /* Get stories. */ if($this->app->rawModule == 'projectstory') { if(!empty($product)) $this->session->set('currentProductType', $product->type); - $this->products = $this->loadModel('project')->getProducts($projectID, false); + $this->products = $this->product->getProducts($projectID, 'all', '', false); $projectProducts = $this->product->getProducts($projectID); $productPlans = $this->execution->getPlans($projectProducts); diff --git a/module/product/model.php b/module/product/model.php index b4a0ada3a6..4a106627ea 100644 --- a/module/product/model.php +++ b/module/product/model.php @@ -330,55 +330,52 @@ class productModel extends model * @param int $projectID * @param int $status all|noclosed * @param string $orderBy + * @param bool $withBranch * @access public * @return array */ - public function getProducts($projectID = 0, $status = 'all', $orderBy = '', $test = false) + public function getProducts($projectID = 0, $status = 'all', $orderBy = '', $withBranch = true) { - if($test) + if(defined('TUTORIAL')) { - $productList = $this->dao->select('t1.*, t2.branch, t2.plan') - ->from(TABLE_PRODUCT)->alias('t1') - ->leftJoin(TABLE_PROJECTPRODUCT)->alias('t2') - ->on('t1.id = t2.product') - ->where('t1.deleted')->eq(0) - ->beginIF(!empty($projectID))->andWhere('t2.project')->eq($projectID)->fi() - ->beginIF(!$this->app->user->admin)->andWhere('t1.id')->in($this->app->user->view->products)->fi() - ->beginIF(strpos($status, 'noclosed') !== false)->andWhere('status')->ne('closed')->fi() - ->orderBy($orderBy . 't1.order asc') - ->fetchGroup('id', 'branch'); + if(!$withBranch) return $this->loadModel('tutorial')->getProductPairs(); + return $this->loadModel('tutorial')->getExecutionProducts(); + } - $products = array(); - foreach($productList as $productID => $branches) + $projectProducts = $this->dao->select('t1.branch, t1.plan, t2.*') + ->from(TABLE_PROJECTPRODUCT)->alias('t1') + ->leftJoin(TABLE_PRODUCT)->alias('t2') + ->on('t1.product = t2.id') + ->where('t2.deleted')->eq(0) + ->beginIF(!empty($projectID))->andWhere('t1.project')->eq($projectID)->fi() + ->beginIF(!$this->app->user->admin)->andWhere('t2.id')->in($this->app->user->view->products)->fi() + ->beginIF(strpos($status, 'noclosed') !== false)->andWhere('t2.status')->ne('closed')->fi() + ->orderBy($orderBy . 't2.order asc') + ->fetchAll(); + + $products = array(); + foreach($projectProducts as $product) + { + if(!$withBranch) { - $branch = array(); - $plan = array(); - foreach($branches as $branchID => $productInfo) - { - $branch[$productInfo->branch] = $productInfo->branch; - $plan[$productInfo->plan] = $productInfo->plan; - - $productInfo->branch = $branch; - $productInfo->plan = $plan; - - $products[$productID] = $productInfo; - } + $products[$product->id] = $product->name; + continue; } - return $products; - } - else - { - $productList = $this->dao->select('t1.*, t2.branch, t2.plan') - ->from(TABLE_PRODUCT)->alias('t1') - ->leftJoin(TABLE_PROJECTPRODUCT)->alias('t2') - ->on('t1.id = t2.product') - ->where('t1.deleted')->eq(0) - ->beginIF(!empty($projectID))->andWhere('t2.project')->eq($projectID)->fi() - ->beginIF(!$this->app->user->admin)->andWhere('t1.id')->in($this->app->user->view->products)->fi() - ->beginIF(strpos($status, 'noclosed') !== false)->andWhere('status')->ne('closed')->fi() - ->orderBy($orderBy . 't1.order asc') - ->fetchAll('id'); + + if(!isset($products[$product->id])) + { + $products[$product->id] = $product; + $products[$product->id]->branches = array(); + $products[$product->id]->plans = array(); + } + $products[$product->id]->branches[$product->branch] = $product->branch; + if($product->plan) $products[$product->id]->plans[$product->plan] = $product->plan; + + unset($product->branch); + unset($product->plan); } + + return $products; } /** diff --git a/module/project/control.php b/module/project/control.php index 15519ac9b9..7047d10818 100644 --- a/module/project/control.php +++ b/module/project/control.php @@ -70,6 +70,8 @@ class project extends control $projects = $this->project->getInfoList($status, 30, $orderBy, null); $users = $this->loadModel('user')->getPairs('noletter'); + + $this->loadModel('product'); foreach($projects as $i => $project) { $project->PM = zget($users, $project->PM); @@ -79,7 +81,7 @@ class project extends control $project->budget = $project->budget . zget($projectLang->unitList, $project->budgetUnit); $project->parent = $project->parentName; - $linkedProducts = $this->project->getProducts($project->id, false); + $linkedProducts = $this->product->getProducts($project->id, 'all', '', false); $project->linkedProducts = implode(',', $linkedProducts); if($this->post->exportType == 'selected') @@ -344,6 +346,7 @@ class project extends control public function create($model = 'scrum', $programID = 0, $copyProjectID = 0, $extra = '') { $this->loadModel('execution'); + $this->loadModel('product'); if($_POST) { @@ -397,7 +400,7 @@ class project extends control { if($model == 'waterfall') { - $productID = $this->loadModel('product')->getProductIDByProject($projectID, true); + $productID = $this->product->getProductIDByProject($projectID, true); $this->session->set('projectPlanList', $this->createLink('programplan', 'browse', "projectID=$projectID&productID=$productID&type=lists", '', '', $projectID), 'project'); return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => $this->createLink('programplan', 'create', "projectID=$projectID", '', '', $projectID))); } @@ -436,7 +439,7 @@ class project extends control $programID = $copyProject->parent; $model = $copyProject->model; - $products = $this->project->getProducts($copyProjectID); + $products = $this->product->getProducts($copyProjectID); foreach($products as $product) { $productPlans[$product->id] = $this->loadModel('productplan')->getPairs($product->id); @@ -457,7 +460,7 @@ class project extends control $this->view->productPlans = array('0' => '') + $productPlans; $this->view->branchGroups = $this->loadModel('branch')->getByProducts(array_keys($products), 'noclosed'); $this->view->programID = $programID; - $this->view->multiBranchProducts = $this->loadModel('product')->getMultiBranchPairs($programID); + $this->view->multiBranchProducts = $this->product->getMultiBranchPairs($programID); $this->view->model = $model; $this->view->name = $name; $this->view->code = $code; @@ -531,7 +534,7 @@ class project extends control $linkedBranches = array(); $productPlans = array(0 => ''); $allProducts = $this->program->getProductPairs($project->parent, 'assign', 'noclosed'); - $linkedProducts = $this->project->getProducts($projectID); + $linkedProducts = $this->loadModel('product')->getProducts($projectID); $parentProject = $this->program->getByID($project->parent); $branches = $this->project->getBranchesByProject($projectID); $plans = $this->productplan->getGroupByProduct(array_keys($linkedProducts)); @@ -667,7 +670,10 @@ class project extends control $linkedBranches = array(); foreach($products as $productID => $product) { - if($product->branch) $linkedBranches = $product->branch; + if(isset($product->branches)) + { + foreach($product->branches as $branchID) $linkedBranches[$branchID] = $branchID; + } } /* Load pager. */ @@ -887,8 +893,8 @@ class project extends control $project = $this->project->getByID($projectID); $type = strtolower($type); $queryID = ($type == 'bysearch') ? (int)$param : 0; - $products = $this->project->getProducts($projectID); - $branchID = isset($products[$productID]) ? $products[$productID]->branch : 0; + $products = $this->product->getProducts($projectID); + $branchID = isset($products[$productID]) ? current($products[$productID]->branches) : 0; $productPairs = array('0' => $this->lang->product->all); foreach($products as $product) $productPairs[$product->id] = $product->name; @@ -955,7 +961,7 @@ class project extends control { $this->loadModel('product'); $this->session->set('bugList', $this->app->getURI(true), 'project'); - $products = array('0' => $this->lang->product->all) + $this->project->getProducts($projectID, false); + $products = array('0' => $this->lang->product->all) + $this->product->getProducts($projectID, 'all', '', false); $this->lang->modulePageNav = $this->product->select($products, $productID, 'project', 'testcase', '', $branch, 0, '', false); echo $this->fetch('testcase', 'browse', "productID=$productID&branch=$branch&browseType=$browseType¶m=$param&orderBy=$orderBy&recTotal=$orderBy&recPerPage=$recPerPage&pageID=$pageID&projectID=$projectID"); @@ -1039,13 +1045,14 @@ class project extends control { /* Load module and get project. */ $this->loadModel('build'); + $this->loadModel('product'); $project = $this->project->getByID($projectID); $this->project->setMenu($projectID); $this->session->set('buildList', $this->app->getURI(true), 'project'); /* Get products' list. */ - $products = $this->project->getProducts($projectID, false); + $products = $this->product->getProducts($projectID, 'all', '', false); $products = array('' => '') + $products; /* Build the search form. */ @@ -1070,7 +1077,7 @@ class project extends control /* Set project builds. */ $projectBuilds = array(); - $productList = $this->project->getProducts($projectID); + $productList = $this->product->getProducts($projectID); if(!empty($builds)) { foreach($builds as $build) @@ -1683,12 +1690,12 @@ class project extends control return $this->send(array('result' => 'fail', 'message' => dao::getError())); } - $oldProducts = $this->project->getProducts($projectID); + $oldProducts = $this->product->getProducts($projectID); $this->project->updateProducts($projectID); if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); $oldProducts = array_keys($oldProducts); - $newProducts = $this->project->getProducts($projectID); + $newProducts = $this->product->getProducts($projectID); $newProducts = array_keys($newProducts); $diffProducts = array_merge(array_diff($oldProducts, $newProducts), array_diff($newProducts, $oldProducts)); if($diffProducts) $this->loadModel('action')->create('project', $projectID, 'Managed', '', !empty($_POST['products']) ? join(',', $_POST['products']) : ''); diff --git a/module/project/model.php b/module/project/model.php index 4464eb27b9..2fc7d8b741 100644 --- a/module/project/model.php +++ b/module/project/model.php @@ -534,32 +534,6 @@ class projectModel extends model ->fetchPairs('project'); } - /** - * Get products of a project. - * - * @param int $projectID - * @param bool $withBranch - * @access public - * @return array - */ - public function getProducts($projectID, $withBranch = true, $status = 'all') - { - if(defined('TUTORIAL')) - { - if(!$withBranch) return $this->loadModel('tutorial')->getProductPairs(); - return $this->loadModel('tutorial')->getExecutionProducts(); - } - - $query = $this->dao->select('t2.id, t2.name, t2.type, t1.branch, t1.plan')->from(TABLE_PROJECTPRODUCT)->alias('t1') - ->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t1.product = t2.id') - ->where('t1.project')->eq((int)$projectID) - ->beginIF(strpos($status, 'noclosed') !== false)->andWhere('status')->ne('closed')->fi() - ->beginIF(!$this->app->user->admin)->andWhere('t1.product')->in($this->app->user->view->products)->fi() - ->andWhere('t2.deleted')->eq(0); - if(!$withBranch) return $query->fetchPairs('id', 'name'); - return $query->fetchAll('id'); - } - /** * Get branches by project id. * @@ -1482,7 +1456,7 @@ class projectModel extends model $class = "c-$id" . (in_array($id, array('budget', 'teamCount', 'estimate', 'consume')) ? ' c-number' : ''); if($id == 'id') $class .= ' cell-id'; - + if($id == 'code') $title = "title={$project->code}"; if($id == 'name') diff --git a/module/project/view/create.html.php b/module/project/view/create.html.php index 4f195e90f0..80565c63d5 100644 --- a/module/project/view/create.html.php +++ b/module/project/view/create.html.php @@ -109,7 +109,7 @@
id, "class='form-control chosen' onchange='loadBranches(this)' data-last='" . $product->id . "'");?> - id], $product->branch, "class='form-control chosen' onchange=\"loadPlans('#products{$i}', this.value)\"");?> + id], key($product->branches), "class='form-control chosen' onchange=\"loadPlans('#products{$i}', this.value)\"");?>
diff --git a/module/project/view/view.html.php b/module/project/view/view.html.php index daf52bfb5a..656b9d99d6 100644 --- a/module/project/view/view.html.php +++ b/module/project/view/view.html.php @@ -140,7 +140,7 @@
$product):?> - branch as $branchID):?> + branches as $branchID):?>
createLink('product', 'browse', "productID=$productID&branch=$branchID"), " " . $product->name . $branchName);?> @@ -155,7 +155,7 @@
$product):?> - plan as $planID):?> + plans as $planID):?>
createLink('productplan', 'view', "planID={$planID}"), $product->name . '/' . $planGroup[$productID][$planID]);?>
diff --git a/module/story/model.php b/module/story/model.php index 9539d9b3be..46fe149a0b 100644 --- a/module/story/model.php +++ b/module/story/model.php @@ -2417,14 +2417,9 @@ class storyModel extends model */ public function getBySearch($productID, $branch = '', $queryID, $orderBy, $executionID = '', $type = 'story', $excludeStories = '', $pager = null) { - if(!empty($executionID)) - { - $products = $this->loadModel('project')->getProducts($executionID); - } - else - { - $products = $this->loadModel('product')->getProducts(); - } + $executionID = empty($executionID) ? 0 : $executionID; + $products = $this->loadModel('product')->getProducts($executionID); + $query = $queryID ? $this->loadModel('search')->getQuery($queryID) : ''; /* Get the sql and form status from the query. */ @@ -2449,9 +2444,14 @@ class storyModel extends model $allBranch = "`branch` = 'all'"; if($executionID != '') { - foreach($products as $product) $branches[$product->branch] = $product->branch; + $branches = array(); + foreach($products as $product) + { + foreach($product->branches as $branchID) $branches[$branchID] = $branchID; + } $branches = join(',', $branches); if($branches) $storyQuery .= " AND `branch`" . helper::dbIN($branches); + if($this->app->moduleName == 'release' or $this->app->moduleName == 'build') { $storyQuery .= " AND `status` NOT IN ('draft')"; // Fix bug #990. diff --git a/module/testcase/control.php b/module/testcase/control.php index d900def98d..ae077935e8 100644 --- a/module/testcase/control.php +++ b/module/testcase/control.php @@ -50,12 +50,12 @@ class testcase extends control if($this->app->tab == 'project') { $objectID = $this->session->project; - $products = $this->loadModel('project')->getProducts($objectID, false); + $products = $this->product->getProducts($objectID, 'all', '', false); } elseif($this->app->tab == 'execution') { $objectID = $this->session->execution; - $products = $this->loadModel('execution')->getProducts($objectID, false); + $products = $this->product->getProducts($objectID, 'all', '', false); } else { @@ -125,7 +125,7 @@ class testcase extends control /* Set menu, save session. */ if($this->app->tab == 'project') { - $this->products = array('0' => $this->lang->product->all) + $this->loadModel('project')->getProducts($projectID, false); + $this->products = array('0' => $this->lang->product->all) + $this->product->getProducts($projectID, 'all', '', false); $this->loadModel('project')->setMenu($projectID); } else From 84340fa6f2549a1801c5ecac4a258499cc146971 Mon Sep 17 00:00:00 2001 From: xieqiyu Date: Wed, 17 Nov 2021 22:57:30 +0800 Subject: [PATCH 16/42] * Code for task#44497. --- api/v1/entries/project.php | 6 +++--- module/action/model.php | 5 +++-- module/bug/control.php | 2 +- module/execution/model.php | 2 +- module/gitlab/model.php | 2 +- module/projectbuild/control.php | 3 ++- module/story/control.php | 4 ++-- module/story/model.php | 2 +- module/testcase/control.php | 2 +- module/testreport/control.php | 4 ++-- module/testtask/control.php | 4 ++-- module/tree/control.php | 2 +- 12 files changed, 20 insertions(+), 18 deletions(-) diff --git a/api/v1/entries/project.php b/api/v1/entries/project.php index b46e169f63..b4fb6cd374 100644 --- a/api/v1/entries/project.php +++ b/api/v1/entries/project.php @@ -46,7 +46,7 @@ class projectEntry extends entry public function put($projectID) { $oldProject = $this->loadModel('project')->getByID($projectID); - $linkedProducts = $this->project->getProducts($projectID); + $linkedProducts = $this->loadModel('product')->getProducts($projectID); /* Set $_POST variables. */ $fields = 'name,begin,end,acl,parent,desc,PM,whitelist'; @@ -56,8 +56,8 @@ class projectEntry extends entry $plans = array(); foreach($linkedProducts as $product) { - $products[] = $product->id; - $plans[] = $product->plan; + $products[] = $product->id; + foreach($product->plans as $planID) $plans[] = $planID; } $this->setPost('products', $products); $this->setPost('plans', $plans); diff --git a/module/action/model.php b/module/action/model.php index f5f9ad45e2..ee650d11f8 100755 --- a/module/action/model.php +++ b/module/action/model.php @@ -1345,17 +1345,18 @@ class actionModel extends model $table = $this->config->objectTables[$action->objectType]; $this->dao->update($table)->set('deleted')->eq(0)->where('id')->eq($action->objectID)->exec(); + $this->loadModel('product'); /* Revert userView products when undelete project. */ if($action->objectType == 'project') { - $products = $this->loadModel('project')->getProducts($project->id, $withBranch = false); + $products = $this->product->getProducts($project->id, 'all', '', false); if(!empty($products)) $this->loadModel('user')->updateUserView(array_keys($products), 'product'); } /* Revert userView products when undelete execution. */ if($action->objectType == 'execution') { - $products = $this->loadModel('execution')->getProducts($project->id, $withBranch = false); + $products = $this->product->getProducts($project->id, 'all', '', false); if(!empty($products)) $this->loadModel('user')->updateUserView(array_keys($products), 'product'); } diff --git a/module/bug/control.php b/module/bug/control.php index bc0ff219eb..779233f9f1 100644 --- a/module/bug/control.php +++ b/module/bug/control.php @@ -541,7 +541,7 @@ class bug extends control if($executionID) { $products = array(); - $linkedProducts = $this->loadModel('execution')->getProducts($executionID); + $linkedProducts = $this->loadModel('product')->getProducts($executionID); foreach($linkedProducts as $product) $products[$product->id] = $product->name; if($projectID) diff --git a/module/execution/model.php b/module/execution/model.php index da342cd500..63279da07e 100644 --- a/module/execution/model.php +++ b/module/execution/model.php @@ -1849,7 +1849,7 @@ class executionModel extends model */ public function getTasks2Imported($toExecution, $branches) { - $products = $this->getProducts($toExecution); + $products = $this->loadModel('product')->getProducts($toExecution); if(empty($products)) return array(); $execution = $this->getById($toExecution); diff --git a/module/gitlab/model.php b/module/gitlab/model.php index 87d00e6bd4..3fcdad0671 100644 --- a/module/gitlab/model.php +++ b/module/gitlab/model.php @@ -191,7 +191,7 @@ class gitlabModel extends model */ public function getProjectsByExecution($executionID) { - $products = $this->loadModel('execution')->getProducts($executionID, false); + $products = $this->loadModel('product')->getProducts($executionID, 'all', '', false); $productIdList = array_keys($products); return $this->dao->select('AID,BID as gitlabProject') diff --git a/module/projectbuild/control.php b/module/projectbuild/control.php index 2b068277cd..75f6248eeb 100644 --- a/module/projectbuild/control.php +++ b/module/projectbuild/control.php @@ -22,12 +22,13 @@ class projectBuild extends control public function browse($projectID = 0, $type = 'all', $param = 0) { $this->loadModel('project'); + $this->loadModel('product'); $this->loadModel('execution'); $this->loadModel('build'); $project = $this->project->getByID($projectID); /* Get products' list. */ - $products = $this->project->getProducts($projectID, false); + $products = $this->product->getProducts($projectID, 'all', '', false); $products = array('' => '') + $products; /* Build the search form. */ diff --git a/module/story/control.php b/module/story/control.php index 9946bdcfa8..e5405de246 100644 --- a/module/story/control.php +++ b/module/story/control.php @@ -726,7 +726,7 @@ class story extends control $execution = $this->execution->getByID($executionID); $branchProduct = false; - $linkedProducts = $this->execution->getProducts($executionID); + $linkedProducts = $this->loadModel('product')->getProducts($executionID); foreach($linkedProducts as $linkedProduct) { if($linkedProduct->type != 'normal') @@ -1651,7 +1651,7 @@ class story extends control $this->loadModel('project')->setMenu($this->session->project); $this->app->rawModule = 'qa'; $this->lang->project->menu->qa['subMenu']->testcase['subModule'] = 'story'; - $products = $this->project->getProducts($this->session->project, false); + $products = $this->product->getProducts($this->session->project, 'all', '', false); $productID = $this->product->saveState($productID, $products); $this->lang->modulePageNav = $this->product->select($products, $productID, 'story', 'zeroCase'); } diff --git a/module/story/model.php b/module/story/model.php index 46fe149a0b..45be5860b5 100644 --- a/module/story/model.php +++ b/module/story/model.php @@ -2548,7 +2548,7 @@ class storyModel extends model if($type == 'bysearch') { $queryID = (int)$param; - $products = $this->loadModel('execution')->getProducts($executionID); + $products = $this->loadModel('product')->getProducts($executionID); if($this->session->executionStoryQuery == false) $this->session->set('executionStoryQuery', ' 1 = 1'); if($queryID) diff --git a/module/testcase/control.php b/module/testcase/control.php index ae077935e8..249d31d1c7 100644 --- a/module/testcase/control.php +++ b/module/testcase/control.php @@ -234,7 +234,7 @@ class testcase extends control $this->app->tab == 'project' ? $this->loadModel('project')->setMenu($this->session->project) : $this->testcase->setMenu($this->products, $productID, $branch); if($this->app->tab == 'project') { - $products = array('0' => $this->lang->product->all) + $this->project->getProducts($this->session->project, false); + $products = array('0' => $this->lang->product->all) + $this->product->getProducts($this->session->project, 'all', '', false); $this->lang->modulePageNav = $this->product->select($products, $productID, 'testcase', 'groupCase', '', $branch); } diff --git a/module/testreport/control.php b/module/testreport/control.php index 28574c31db..541ca1c4c6 100644 --- a/module/testreport/control.php +++ b/module/testreport/control.php @@ -53,12 +53,12 @@ class testreport extends control if($this->app->tab == 'project') { $objectID = $this->session->project; - $products = $this->loadModel('project')->getProducts($objectID, false); + $products = $this->product->getProducts($objectID, 'all', '', false); } elseif($this->app->tab == 'execution') { $objectID = $this->session->execution; - $products = $this->loadModel('execution')->getProducts($objectID, false); + $products = $this->product->getProducts($objectID, 'all', '', false); } else { diff --git a/module/testtask/control.php b/module/testtask/control.php index 78f410e170..b3b8d0bcc9 100644 --- a/module/testtask/control.php +++ b/module/testtask/control.php @@ -48,12 +48,12 @@ class testtask extends control if($this->app->tab == 'project') { $objectID = $this->session->project; - $products = $this->loadModel('project')->getProducts($objectID, false); + $products = $this->product->getProducts($objectID, 'all', '', false); } elseif($this->app->tab == 'execution') { $objectID = $this->session->execution; - $products = $this->loadModel('execution')->getProducts($objectID, false); + $products = $this->product->getProducts($objectID, 'all', '', false); } else { diff --git a/module/tree/control.php b/module/tree/control.php index 2b1d299674..fa0d1fa46a 100644 --- a/module/tree/control.php +++ b/module/tree/control.php @@ -258,7 +258,7 @@ class tree extends control $this->view->root = $execution; /* Get all associated products. */ - $products = $this->execution->getProducts($rootID); + $products = $this->loadModel('product')->getProducts($rootID); $this->view->products = $products; $executions = $this->execution->getPairs($this->session->project); From 292b09bb28c1602fe1782236cef21895edfc745a Mon Sep 17 00:00:00 2001 From: xieqiyu Date: Wed, 17 Nov 2021 23:23:00 +0800 Subject: [PATCH 17/42] * Delete unused code. --- module/project/control.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/module/project/control.php b/module/project/control.php index 7047d10818..0ff8adfee6 100644 --- a/module/project/control.php +++ b/module/project/control.php @@ -666,9 +666,9 @@ class project extends control $this->project->setMenu($projectID); - $products = $this->loadModel('product')->getProducts($projectID, 'all', '', true); + $products = $this->loadModel('product')->getProducts($projectID); $linkedBranches = array(); - foreach($products as $productID => $product) + foreach($products as $product) { if(isset($product->branches)) { From 000728b47ef478af3ad8e7580dbad274888ceba5 Mon Sep 17 00:00:00 2001 From: songchenxuan Date: Thu, 18 Nov 2021 09:11:57 +0800 Subject: [PATCH 18/42] * Finish task 44490. --- module/build/model.php | 1 - 1 file changed, 1 deletion(-) diff --git a/module/build/model.php b/module/build/model.php index 65b97d3048..57ea164118 100644 --- a/module/build/model.php +++ b/module/build/model.php @@ -295,7 +295,6 @@ class buildModel extends model $builds[$key] = ((strpos($params, 'withbranch') !== false and $build->branchName) ? $build->branchName . '/' : '') . $build->name; } - if(!$builds) return $sysBuilds; /* if the build has been released and replace is true, replace build name with release name. */ From 1ea20cb24e14dc3c23a14fe146d29859e5b1a659 Mon Sep 17 00:00:00 2001 From: tianshujie Date: Thu, 18 Nov 2021 09:25:49 +0800 Subject: [PATCH 19/42] * Repair the error reported on the plan linked stories page. --- module/productplan/control.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/module/productplan/control.php b/module/productplan/control.php index 4b4f947742..c886b509c4 100644 --- a/module/productplan/control.php +++ b/module/productplan/control.php @@ -415,7 +415,7 @@ class productplan extends control $this->config->product->search['style'] = 'simple'; $this->config->product->search['params']['product']['values'] = $products + array('all' => $this->lang->product->allProductsOfProject); $this->config->product->search['params']['plan']['values'] = $this->productplan->getForProducts(array($plan->product => $plan->product)); - $this->config->product->search['params']['module']['values'] = $this->tree->getOptionMenu($plan->product, $viewType = 'story', $startModuleID = 0); + $this->config->product->search['params']['module']['values'] = $this->tree->getOptionMenu($plan->product, 'story', 0, 'all'); $storyStatusList = $this->lang->story->statusList; unset($storyStatusList['closed']); $this->config->product->search['params']['status'] = array('operator' => '=', 'control' => 'select', 'values' => $storyStatusList); @@ -451,7 +451,7 @@ class productplan extends control $this->view->plans = $this->dao->select('id, end')->from(TABLE_PRODUCTPLAN)->fetchPairs(); $this->view->users = $this->loadModel('user')->getPairs('noletter'); $this->view->browseType = $browseType; - $this->view->modules = $this->loadModel('tree')->getOptionMenu($plan->product); + $this->view->modules = $this->loadModel('tree')->getOptionMenu($plan->product, 'story', 0, 'all'); $this->view->param = $param; $this->view->orderBy = $orderBy; $this->view->pager = $pager; @@ -554,7 +554,7 @@ class productplan extends control $this->config->bug->search['queryID'] = $queryID; $this->config->bug->search['style'] = 'simple'; $this->config->bug->search['params']['plan']['values'] = $this->productplan->getForProducts(array($productID => $productID)); - $this->config->bug->search['params']['module']['values'] = $this->loadModel('tree')->getOptionMenu($productID, $viewType = 'bug', $startModuleID = 0); + $this->config->bug->search['params']['module']['values'] = $this->loadModel('tree')->getOptionMenu($productID, 'bug', 0, 'all'); $this->config->bug->search['params']['project']['values'] = $this->product->getExecutionPairsByProduct($productID); $this->config->bug->search['params']['openedBuild']['values'] = $this->loadModel('build')->getProductBuildPairs($productID, $branch = 0, $params = ''); $this->config->bug->search['params']['resolvedBuild']['values'] = $this->build->getProductBuildPairs($productID, $branch = 0, $params = ''); From 8553a87f00f266b5d028eb94aec88c5237d3ae0a Mon Sep 17 00:00:00 2001 From: tianshujie Date: Thu, 18 Nov 2021 09:51:18 +0800 Subject: [PATCH 20/42] * Finish task #44502. --- module/misc/lang/en.php | 1 + module/misc/lang/zh-cn.php | 1 + module/misc/view/features.html.php | 2 +- www/theme/default/images/main/tutorial_en.png | Bin 0 -> 62082 bytes 4 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 www/theme/default/images/main/tutorial_en.png diff --git a/module/misc/lang/en.php b/module/misc/lang/en.php index 88093fceb3..90afaf6003 100644 --- a/module/misc/lang/en.php +++ b/module/misc/lang/en.php @@ -85,6 +85,7 @@ $lang->misc->feature->lastest = 'Latest Version'; $lang->misc->feature->detailed = 'Detail'; $lang->misc->feature->introduction = 'Features'; $lang->misc->feature->tutorial = 'Tutorial'; +$lang->misc->feature->tutorialImage = 'theme/default/images/main/tutorial_en.png'; $lang->misc->feature->youngBlueTheme = 'Young Blue Theme'; $lang->misc->feature->youngBlueImage = 'theme/default/images/main/new_theme_en.png'; $lang->misc->feature->nextStep = 'Next step'; diff --git a/module/misc/lang/zh-cn.php b/module/misc/lang/zh-cn.php index 7d7f6c5c79..bd15484124 100644 --- a/module/misc/lang/zh-cn.php +++ b/module/misc/lang/zh-cn.php @@ -85,6 +85,7 @@ $lang->misc->feature->lastest = '最新版本'; $lang->misc->feature->detailed = '详情'; $lang->misc->feature->introduction = '新功能介绍'; $lang->misc->feature->tutorial = '新手引导教程'; +$lang->misc->feature->tutorialImage = 'theme/default/images/main/tutorial.png'; $lang->misc->feature->youngBlueTheme = '全新青春蓝主题'; $lang->misc->feature->youngBlueImage = 'theme/default/images/main/new_theme.png'; $lang->misc->feature->nextStep = '下一步'; diff --git a/module/misc/view/features.html.php b/module/misc/view/features.html.php index 6a622b8894..2a23a7acf6 100644 --- a/module/misc/view/features.html.php +++ b/module/misc/view/features.html.php @@ -35,7 +35,7 @@
misc->feature->tutorialDesc;?> - ' /> +
diff --git a/www/theme/default/images/main/tutorial_en.png b/www/theme/default/images/main/tutorial_en.png new file mode 100644 index 0000000000000000000000000000000000000000..a1dc425a907d9e1735af0817cb016c8401e88f2e GIT binary patch literal 62082 zcmbTd1z23m(k@IOxVzgxaCZoVV1v68Y=D8mC4&Zc2pXK=Zb1eK9tL*{8VC+S6M_ZL zo$S5O`M!JqbLIKxnR#aQTGdruZ&!8K>aLE_(on?3qQXK#LclGi~(da{oA31Xlj z?zlx_79#$hyw*{aMXH^oIY1Pi+R3QPAR#p*V&8*M5oJs_CF9pfNJP*-zb9+9^gf82 zlu!jDsIIFm)EndhMv}2{wE{CJJA>@NI$)5EulopC5(x<%Vy|ZeHBwiTuy%Fk0sW!j z@o{!T)J8&*l=g80Sv!KE3|3$}dlxCdkB(0O274PRfT6HDpSqhI7-FyF=Kz>0Z6}Kko1v2FmMJ#K@2|5PA;z{e53$>@s&Va|GCWzVEBs!>L>;HyHQ5!S`2co z9$*Gx9uaP9ejzaiQE?uA5g|T75iSM+K7MgtJ~3W?L2f>N2|f`CVNr&EJ^+L?4;xzv z9eLnCvJfRH00atkli=m`_V(uS7UXgDu;b+y7f0|A;1v+yMo@6S_H}`Re7Id+GyRK$ zJovS>hrJur-qnTS4@Z!dt0zeBUWjHg{Y#tIP#y4p;rahi9YOwAWj6^q z4=@Po>Y?ZA>h!N(*7}#e3<3zq7}yN$U2I&vU$g&__D=<{JO~Pw0toO4@N@GczG8ZO z;u3tq62iiqd}0!Oe1DUwyV}^>`u-+0bQLZD^; z$3=METdO7!>S}t}Bis(;^q+VL2!O2w1cgPp#lZYF z+#qowD{c^2)P~#2+RDaS3VExBy}D=45XT`lC3n zmy69mW6QsB1pII1yasvwpCJ+wvK0^)5wzkK6W|x(76uE6a)WF|#kg%ntwliAf?!cW zE0KRe{GT8af z8&PhsxTuJ@0KW)WluzKln*ZOXi+?!)Vh;Lq`R~aOQTX@t33fqf?}3;cxjjvBkdO`o zmE~phe9Vt?QG-eK$%lT|eSG`1G~+waL{IP707qV4VW6j%OnPN%svn7X0MChC=4DAo z2esGoL#-@4lx&)2%m$Ye(E!j|8B7ymZ6ll5HCb&!OiK#Pq8D`Kkxw4f6qTZkwq zVv_okVj-@@Mk_X||2!Sg2BKR0^)Nwz8_Ei=8;6<{wQbBMKT_E!nOP@YGPdVBi4?{n1C9z~4V;ZS$Jb(Z*X_CMkPeS#IKwZQ7V1l6foKn^P1stv z3@_%-`B@J}`5JTIM<(i>a2-Y}s9K4acqwsB0hd6w`e=@W7m!@|c{$o^C2AsTO7_bi_UiCv!1r!2Vla?nfwBrzRhWo>lR7@&(mskrn=8g z**&@Ty|_ssSp>A8V|>x16Pd|X5hdj~fcnn1St)$+n>(XmImRWL?GxoWK`B0M4#Q|C zC4eIlOISSE_=RkP2WqL6-K}?^Ve%uuB=J+x+}Y@RdwoDW8Gsj{U9 z#;j&>CkQlc%0%0?5R(&GJ6D&_auQbp;|V9w=;l0VU&fXb;N@e#s45Oe{{BP}s|C0?FnV1^@g9oiH6ViS zU#gdw!6IQ9brAxdHHPds79%-{BZt6x62+N_rklYWTrCh%|3rcp`S^i$gfCug!ktre zsxZ1`(rQ10aW{)^u%4H0c4xQiSQKKPlLiTOO(z$O2~i5dUKnRgYXb?v=6*>Z&B6BB-kA(C$Wtd{KwLmVKneXqq-8<6*J6C!_)XsoYY)~ z<2+3xXCK^zb4Q6eB`8}?IyaFFm2a&hC}jZSre8klnBN*?D%z*QfXo)aU*)L^A#)8C zoN@ur+BHV0xQ@nB3pP4-kDzlOvT)u@acuppJ=-|)vRYh2frylZhw|!kkca2N%2{5I z`gU<7vuolO^+0T|iORDCNJj;0zHJnEnzyhrEHfMqTx^*D7dY=+QpLfLJO#eDOHq7A z+~cNsq!)_gC}ZJlW%G{mEp}>@USMsqWIw$2(mQ2McBTL^;T2HvhOhN^z18F!?4lyB zrOySlBF!i9Qt=Xu9}UntXGZw$I-}VqWp5MaAq^fPhtB4)7vmGxoZTpoua5kj5=IMd zAwbseM31I5LYqzS0TwV3OxM9ah9lKNoU;9O@*MJ3l#ue*QrR7P1w_J{uE_T`}o*hlv4gy@y3k9wP zHczyR?qbilsE=jUVQUsG*fNcuNpx6O`tu7Coa2n7W%&CIYo!-roYh8!M*Jcs*&Ms$ z*63x^L9(~i2jQ^+Y{?IVA|hHG=*&30K%fO(_(ht6&715j*GA`CNy^Kk5`8Lvd=I^O zstjBT-KR*!*xt2 zI)9>Q?9b8SB|-ja{ldck=YGa@;P7*ZKXP zF5G25oU85^`?xU|e)djCmkJJ;^I{S5`8Oo|CKi^~#ClsivbjrP5VNI|B!4$CAwA9iWH^j zqUWMG2jZykxX8~9K`zncnPi!un~yn*rehC)LekVnqVKPV*)oz~d1xtBm~}>boRxCz zhwS3@iN>m)O@0@`nL17>x!KAHdP`xYeK86(9XyV>lYR-xI^=iOx0( zj+FRn7m`p!mv@%CNS3&c=03_LW5&i&pgO`5;G+FzuEEf39qEK=8z-BJ@pdCqp!AqvGgQNZp&SM$=Xt*KI zZzV5{f&2RLv<3!eD#j!IqWLk5X4{m2&qvJRDz%)C#mmqRlIWz%KCCcxol5nwqiVgY zS90x9RYivYQo3`EDYw!uhtd3O0ty^SE4 zD@2_CTX15!TKFg*-UM%GP9qpw);&t$feeen*;8MKl`SupW zjQ;z}=rTm8QS?|qLD;nfmndL7WTHPXSbiinS5F&bN+k?wBA;}$Q#b`|H|iUX5{%D_xj=1jvw+v>|*M@{KP4Skx)gc2|Wob+3kxe zLJ!Xf{93g4OOH0@s+U|hXCh2z_lgVErLw%6eKC3v5wB__c2C0z3$hfnueJJ?$Mn_m z=Ue!km+~cO%?U2}q7)UQwmHp8(JoXpaot6qN~S$alAPTE$8*8%be99=a}x^R9I;&DMBz^`Oz z)xB@SnaJYdBTJ&%;OjGn(uPlrM^hWsM|iZ~XJrVas%czIe*~vv9wZ?!lf(3`+;Gqf zr*8ItcIZ&dJFav&qDGn)`CiNdF4dWLs>jqx^No9kBJ&*m@ckM8xBFMxsRr1#`EOfT zKT(Kvh!xCM34FdiOJPVTC+kr$ct71Rf%ANKBK7C&VE?Dx1a_X@(Q_9WuauTni95}b z4gveLS7FX)Rro*NZiz)0=nc2OIaFCHP3K$G>g65N+ok!g8h)WHtKtEf7Rc{w9xb}xou!+Sj-7Yj{ODvsYncB7E07_XTuCzc#1+rzUnwNF*? zT~!XAy`1ScDp;z9)}ysF!g8^Y%i%`3@8xwQ-Zl@BxD$R>av!lR-7{va&alV>DVE2_ z;&eIeiZ>iW@yYTNM?KuilKG`))L@BT9)TFMDvwWQv)CYgW^wDafmF0AA2;G=^>>}= zoG957m>^P=hG|BwosXf*)0Y(At4opPBFDlrh>2bExnNRkXqe}_PSIv^>BJ%vGPfgF z&{yG|WmvZk0m**i%FW@PG;XqctZ}1Gut|vj3%`d$HEXGX8djJJgpB%7X* zEBz(AoO*f@>gmJ|6D%uLmMMNGiRq-n0>~})D@>Zn3yC=#Vi=`xgt7Ja2XQGWVoE&q zDmJqu{}RHYphr>9O@?BgGu$r;xIn{MH zBkq;yx6}fIesWdOTGLDEWwT?$>o>C%zb<;8Q8ZYS*sh7uIBIs$m}6yX2d1=sIw~n} zERq#kBmD;Q8vVg}rV}j8#J(2NNhDgNY!3($qz$%mS|!N8$xb%jkz@bx<54|e|Mpfc zvuQTs4b{4$OfHizN$v)1VTMyhKGg%0&t*Q9IKPdD1VQP;Tr}Z|>vc=%0n}LT8q(dX zQ3yYnpK0>k_NDkGU#Nb(b9p7obWl_eAKv5vdrY~^6DYRYuT>{20 z(jtj7$bIw>nw zQy(=x(O?)6Q(Y0pu&wDQQh)X-gyZOrt`r!Ky;)Yg^{9%G$kup?7WN9m^wTtTTzds3 z>6wm98jKBguXq#;=kgbBPPo8dG(_j&JgSRx>u}GCJsXTvW_$n#UEWwL*^k3&$QN;D zEm9YCtQlwMcI7Voo|On(N51qEIj~iqeyGIzTKha=uk?#0UV1sqDZ<@q-tcci-MVbyEO~;Q`_WFG+Y)c;p{E^AXzZ+Jbkn*?Qc3YzK38cHkPk&xvG-e zJfa5Eo!-LKDfx@t{!o6`{sJ6}KtJ9lzsuV!H@n$J74Mn*k`87`SNS|`caF+4?`{F& zB4!&!4#rifPP#Q2qtwcNHo>`%LYj2(9w=-o)v8QJ^pgJKS!KcENYvbQkVf)1=U>^l zIQp!mP+FT`NjePpO>>M!TpoQa`e;utdFNeZ0pzd!U=jKL{oPN~b&Azl#$dFcfh4;f zT2{h^Wf?2t!@J@Gn88c+p#b7{xF79(CESY}#i^v;yYCu{NPdpLSWeY!-gq4QKwoe?I6kF(#eok}jjJwF+6DREiV)ET~F8}yYWTq|6nCMQ9L z_l~K0V^?e0Ysx_!KN*>PrJ7?b#axX;NIv}$5e`*q8OXJiuf&TyXxj958Z6MQh>=o0 z62~^hfSsBgVYOI|BUeVW-GQ>OUPd-7L~z?X%3o7tZG#*1#F@Sce%7@zx-gIogLBN5x z%j4gYv&}leLVbPSR9ZmvaL;9)oQ2`oWODIo{uz0A6Nz~Swy^G2!w_ZsRk?`#r^)7e z!No8#O0S#+E<*Gcu=Sv%0&6VxXZGkuYZlO+Ng65s5kyFKBCZ{wV?Y`Scc zM6i}9nGm2}tky-9`%Pfel0i z(5n%&bEU?Lba`kroqbqEukPnATbTbuFZ^l%==lL+HWh1<76|E|_CY0wQF@+HShDGK zYxMdv!Mh3X94q8BIV#oYEMzTn4P3ziDHv&c)D2a97L!y*aq{|C90vFq*yb;HBLSX! z`OHf!zhOICU-sAxg*zKO0wk?-&X5VP^{1wx1pTrz(MD<#;fvKHgJ(G3N*&W6sHLCe zU0QF^F1`PL^Du0u(sd@2dpqpM9Jph=eQ*tlWd9Z1+V!Kno4kE|>7AJhlRkOgi;rZH zmoajF@sM$o{I_Pt%X|t0#V%&g>AI_g9p;r3hpuqlG%|3WVm(|tFQ7TCwbH#2{m~QR z$*C=AE#x(IixM#-Tp_$nxhyl_j_*1~+QJydv-8q%wFQIjK^SH^`W27s)7KDbzxUJZ zQ2<8TiP{rLUjVzzN2>S}pKoO7=`RT4BQlbDi7VA(Y_0WUNY8(~g}2{X*KdX_o!L`p zx*Pp|i6gRp{4#?G5o{tBj4NU}Hkhg?Xmw4nn%a2isv`%izh-#}v4a$G$##d$;%pUA zWtLcuET5Mvh^W00!gQolAGDAlc0&6sjrFB;ZY6XGcKy%|4C9)co3Ei9UT*4 z0#L*FMnSJ4cI$R`1+c_Ngs%-Mbk41Ve490fw9r2e4cTkwU4lqQw%dCkOj7uP7XU7b zjgvTi*@>XIfC7Id!N@pB;#tj>6=Pud9A-c($~VlJR2J&ws;Y!;ozsX>_Ab}A@23|hYx|;b41RA z(;~J)&9grt^z7G0BmyS8eK9o|I%I9sKh1^w%bsd$lJ{L$QUbvZjKRBtW{@*CLM~0x zzN;4O>RE1x$x1Bnp2zn)J*x8SP*m=t%w$44pb4&^$xnX*R3Vd-)#!YW0!egPsuwxC#4k?lsC@ne@k@L9Kjyi%N3xm;TwPA1Y{>ecUFiQTMO?o=j%{0KWL`(SM>S zF+bCkP8zQuMb>}%#tuT)rr(V`gTp^k~y}I6Q_CQ zFVxA-gKT1k(RoWCoP`v3ivw9n3Kh;T)mH%LR$xqx$V}UI3bb-}NpTZ5JRl;K%`y&C zP7kHYH0#w?c`7x$w_yv6w0K(-r&d6J^y_i5xqSX74Ve5sKVu_(_T#83%wWVFc&Tt_ z&G3oi!C2I{_0D!=t0Wa;E!=}khI9i=$6g9j01Z^>5?27A8|Xp zf52B2RHtozu#77?X`W0IvijgEy7#_3;8LAH9XAtIF;G=6~|=0*2F<`Pl9Ht_hU*U(oEVs<@Z%)lFPy zx-dZZi)0 zcv^~OpV)a@n-FtLOi|*CdHTs^;p0<$wRK;&g;}I8zhIkv)wI`D?=jg0vLn41GD$ho z?YD|?)z1q041GV`3yK(M=?rQb5~E*tV`UUS`sT`*g-j)4p6B|2?D+CK7UI#n%fmR* zg7#*_R@I8X&gw_-Nohw@x7^GWhoI!YphF?~9D^JWX-h2ylC1ggqL)Qs*IVJ|;y|8M z6~xt)%Uc_9Qn$EzZpCGc*quJPS2?_mFX9j56-;rh-&NUoq{?WJsj;+<7{{j8-1Jhn zF!A z5RrQNUgKNejJEucBXom*1fp%Fun*4y?;Z_%SkR|)7hFxOv09-&kCp3AGfL?Y7SEbkt&0OOd4BwAREp%8d`rU?q>*PyRZ*`E`0GC6h zeCADJnazwvq@jb^TUe;h^@|&d?=abt`MQY61TO!ZHHx@<=VyMo-c;?PXIt~2#H}(o zeh<~yFaH&*H?=57-X}orJA>6A11yabQ+gI}Oc>is@-IhQ3v)=dT&tlCm|gYaRgA)% zb;)_x4#47w5D309EBWL%sf{*}SmAPOa+b5-xv8BH~M8oHw-fbbzQEJC} zFW$L(eWU0Md@aB;dwf3$Evw2ys~lxoE!$-x6ZcVtTLXo^R7a|^MFzYmk}@@(K(lGq zZxfua;Q?$uk$UQSN>^LcKbDcMJY<-BvnkVEcT|GI`*{|&@nE*lRuPxj!XSErbQ1xG z-A2Clp^>~cXbZ*s>!q{g&nlhVjL7#})pSlrOl23l$;Hh%bYk8D3LDv)Jccp5OaEXDZr0tHF01w4cq#3&R9h3 z_wbNZo6MW*T3Y53CU`9c|DG{kgD!BdjS7VCj&UwpLB&U$R&gs6~wTN{w9MZR2 z0-6zR{ywzXj=ohlBYKJp#F6V^(!}i>X+Y8Ko_&6?)BY7m(4#YCsYJZ(O~ch9IsauH zWrZ#;rF*F(L572D*oA=UY6ri8t4VDBP$K+ZzuHp6Ih1UIdLy(qfTH0}bLW-gM*8iq z8LP5BicfFet&lxz36)sK5~DRBhXI&ceAUxMOy(D!3YM+ez%BXxaH_1?f(K#aP+ z{R(Ik_p_IOZzi7=O#Rsi1lxMsq9+kjFQ1iY_=i-RCB?j`t*w{Z|Mg8G0r8C zlS&HnQU!7 zJjAMbeK)%mx$gx%=Nt>O;uCDL_LtU7{lYQu?nH;a2PsA&kOp|Cg&gc z60=?Vt9t##9h;(F^L2D%jtutlT}?!1p|GUgJU-jFeILTxjfSSPv5Q{kLW6Z&hRfhh z>4uNG$ZzokJ`fG)1}{hWf(h9baHRd4xi# zj~z|!0#JMCGGa<#iuoJZB=2c5W!P+m0137%mUd9MpD~%UgE3-XX@*ds?g;l1Lp!}V z)i8BjRrUJ|l+?Q|ulIU3w-Pr3F|ik}@B0FdGfDrac##mrNi&&!Xh!sr_x zE&@+az5Q=axcu@foxi7K;aQQv^u6lENgyR2=WSkds&9(};3_Mpa1kp7&y8R8ecuMp z9$v()Q>n@so4c!B0~I`m9p(gf4C;pxmsixpK2}ps?2G9cQM~joLbc!Wb759nI_)49 zox~{!|E1o>p^#?M)ydt!)(#I%XI(8>ZJdh2eD80RLmvE|zP)I>IHl-!$-@(py_=tl z8q#DnZiVYs^vG?2aqAlcoygK&ygxyPP$bS4p9ZHW{FIM(NgXR;{70R`?0A+3c)WYA z9@ae>r4(<1smj$eE#TSudqeq-QkiXu&{etv)49FD6Bo7B@-AUzg%<)kASxG?_4rE}Vt2^?k_KQEss=&8Rq78(yC;O>jTq4M)B zW*UjCx`-1C)J8X=x5@~$<-1}p36hVH{9Pz;rcykfVW^Qb&cO(?G{wlSQ)-PX%#mD! zsVoUwJSokYm+zvp-|OV!oPL@`{u3T*JKT%(TlO83Q)_2UYjv#-)yORW{6Pus3~H+O znkw3hke$1p2olJ^gE?N&3s?%ibnvjWGzYVV`rPc*k5q%%r! zi4hSA-enkIyhl(3`9K)eArQo2Su^H)L%SAf|&(M=9sC23S9+}<|#&e5TC{#XI z8z$e>OgkM3?^(#V_HEb)K~Yn>Km}(gV7ExS4%5X|s0CQM(Loi!$6rS6YyvIWo#Dxs z`8#c9-_oTxex&!+38?5r&1vGsymZY*ZxXwGaM+J!Vyics(qH^`_1QE=2O^10KM{44 zf0>MZmhoyjG_q4%XkJa|?Mg8a*<5;oBzu}|ApG4WxZ(zLet>n)w0y~{$*&Dl{evBp z7W1r){E;JiDZKdw?489b9ACbHw~zgIQO1@Pcr@RT`%bBw2=px#6_I&H0glnKN0mD!gaR!z zJhicO7A%c_@a~q@O)DF#8uKl?g?N*)_^mS9(;sSfIb*O5sjBI_-*^4(gySXVNzfT4EC>F_;*5QP;lNGuR0G9+ClP%At zZ7qTJP%f|jGElrQN`$$@%kx3THMy!2|IPDBfy9a$$#+E?W9wX!rh2Cjifs7Zn-;=F zo+C`ijH8zhN#aak4KaqZvSjt6t~OF^wB_k|m?}iir=W%O+vl>R#+mpzZ#F97f|C$n zIUMO32kyc%`48smw5i5J-6rNuW0XKqh##3SHNACQcY7{Np;bpA-2~UY?3Ds8ql523 zU!`_JN%3zPBfhyYK(*0c+&(WgWjbzm-07l!L~a_F%JYNL@%`>>8XfZ#x!W-bZr+N> zy5n%@u3qY*VD>m*EC(&+>}K9yZd94}8FD)AC8Wq4c&%(VVW21HwM#9n!zI5n zu*#wHU-oG#c%gOrK@;x%5u>}wl_t54%6-6!#eVj45{`Nr{OU}epdk~Ap-K5pA^kVR zFCgM+V;a-Lm_o>Y7?-OMRc=*S0~fKm;Zi>)lMc7V!C6HCPL&PE*iU-=+=UFXt@yk5 zo1ZgydgrIsB7X?f_9$W+ZlpAEo;n|kXGUGIUG%5CVOQ74-jOkplCkQ{msdtej` zyl+?=B@ih_88cy(Uq0C9+2%ZhG^mwMaE_{*v~(NT0LJnTx@*A{uoK-s1)`{~N0}l5XzKn_oXU z(kyhXpea|{@NyKLz~Y)DXLlVTrmP2H@v|k2UN#>zm^OU}+_63&wh-RIYOz033qG|I zFF1R))Qhxsy&9p)^6nk)N0-a3L2pwZ7p4(^al<&DHZ`MM`fhW2_d|%p0I_5h z*~O?M=2T4nD2iq-Pf7po_~@I;(c029?ZQvMRu^@f5?AKjnMDY`;)P*~Cl0Fd+zDeA z!{C+d^ck>u%_2-E8q|&UI-0j{nNJ^Mz5yE`epAn^MU7IE!8E3maRyz*CYjdrxh9Tx zVA3@GMEq(3*Iju-Wrr~xdX^dBhW9(KuGO{2OQnQn9K!zAG~u(Q$kT<_{JvBKBSX>0 zVX2hXuLp&QbX+e$SwA>`W_5OpRaJz~la6QW95RuKQZ)&!iprNo0C=7&iYZ#rm*Zbj z^>3}Pe%Y8yzt8)me~gg@&pcxd zPbA*{A?!SP=+%LgMVm&cTJ(I;$StN2Yj&Lrt4s-+g)h;dUluusHd_UGk%56OI=J$) zP97BfkC!JWnKi*FKjg^A_D)_BFnYa<_@br3y!j&gE1Rn9akd6Vk=4XxCCd>dt{E#> zwiN<{H?L~oF?v1rNobUqA2xNSdUl9l8WF@tMl*s-8xisK1ezJz#%u@UTs zSw)){h(~1*`Wh$cFupUjr-ul%MlFz+%_$sMHVI~%S3UC+m*c>AO=3KH-eqnVzA^Sn zzYv|aqtx;Yn>IO*Az!{SdW^bff@xvmFR`)-=a0bJK-$nPK#g-ar&|pfj*d$JyExg<&q&WZKF#gSoQFIf<3N@I8%Z& zgR>;&K!TdpOXnESKOMm^Ylx?WJfSs)|Asm_N zraP@7^amjjzgsnz#6KS2l+Y@`Va&pUHIkHhQIyGl$-#NYq zceqEwVKu7zY{?jJC}3*QykjNA4sw(TlV3(F*V)_7Tw27DU`+XBPL7?;1=U#)$RV$< zxk=|#wh<4lYcN*^pC|*aGA@$+J)f5XEe`r)%z>|1kL{Qxr^203Waa7$^liBSqtq9& zsiCRDMtByFxAV&Oi?bYKs^S$q|0&y7Y;~g};MbF;V~eNE_VEX~C3>kv()SX*KGL<~ z#b{KeUp7cB{F7+$G=KBJ@K{V2_(aaVvjq+Ay!q1Wnw+h_RqdDPok#BNt>2`3kE^nZ zO&ktveOEnn%sxDyj|(trm7s4qEzjudVAdhJjyJr2zMiBmFOlkf)J*>ixh-{*HxNT8 z>~#K1QSy(b#FUwiuJ&Vp=9w^8a7M1L{&0YfDaa~|AVg+V=%ARW5c3w3cC6-`WI42+ zG(`QJ95F29g2Q+X2c5^l-f1QGq^!{!IQV&O)mVJS40_R;!Z5~H6v#dA$#q6A+cJVd zuhA@B#G4u`(QBiO+EIr8gZHhIb(SEYPFyutn{2n#jB`B3NIKjpF%1BIKE?&BDV^oH z8;7|pI^_^f#X)=*usim~!atU;7+W@9qy=)ltNFEv$bJ-xm}AGeXRSziAU$Gbi@YAa z9BMy(jD^ikKGs+ppM&LtLalr$0aiiH(m82=rQwdQq$d20;EM-b zavBN{t9p>0HJ{8wFC?3C6{<~F-NjZN_q_!3fbc& ziV++0b9ICO$Dmd`{vsv|-)Wn;ZX)Bd*+;4i0RMHp`ImTDi9l{9zq)O*K9}Ks-91yT zO>Yo@D~P3p`-WBaP2Bft_K2_b_sVl&k@dQOvA2&7W5z`YRM?8g!nM;e(hT_e>16CK zcJ;{r&N;u7^F4S4qE11W7AcL?vgUl@e_l8tf|3i zYZF&eS1!5bXw`bolA4}gSW-ekiTmEcWZ+X;SI5WCA|RlS4v{l&WV}(Bvv?b-l8Ocp z+JqSGV$U|-hqrJqrtJ=av*!yOGDUrN=d2KIbJAgYH;ll4v&tTqpB z90&u+jCGDvNVk7C2HcN>?Qt>~BQlX}fESA!)s>g+^3u>}4~zbnXK2PtCA^G>NxSSH zfTtfS_~RuZZ~HDRYSBpR0#uaKERh)Cg{bfm9McpmgtW&z_sE?tho zN?gy%&XZIj?5b*%%2`&MCst_HCAjg{$w9=oKBKmGl2RR!bmbMoXw5|(T4)Z!_s05& zi1KQxaYW4NJG)Cc{2zvElx`AV^o85bIfLp|G|~z)(k`iTJl}_$K1SBjo7I&{kdmYo z0m^0FbzY)lZT}o)!|pu+;rSU5+vMp=@x4{kBGEF>hT?Q@e&59Sljcb*KYj2Z%!b%K z71DX%{?&*ruF7d#W}Fd^)L$7iH_{@O{2)A8Jo8bYBxv}a>d-Q5*g(g)^t}2Y-qw1Z zSvGEi6SFSAQIZz=z9Jk^vv7%N;!WDaGdLu@8~By6D75ZROBAEA&_Q7U=2vOlIc~+{ zucMN2@-Q-pLJZA%l61d zxu+t5%VK|l+5JokLEOoHcVuilIIbIUR^f#5u2;Tlu{BVPKYd$y7$iZ<%{1B=mila# z2m7^Elc0@tZUb)2jA%(_`erz5m+OV%u#SCd1d3ym{AbD&H0`@$*8A`0m(ryLpsr6h zJ!x3ov4=&WKN>>9T#fB9TYgOSwaC7`zBLw)L_uU!yN&9ZDnvy|vi;nbmG(JQEbC+K z%;u<0u)*N$0aRzI998lVa_}wiiXVI7Q2gSTVlZga!pn8gf0i33NL^T4|o^Q~weL@t1-z6sXJAO9uyzq9VOh{rj+rjWFzFa6$~5 zs@!P0ApvDZq^8uvlm z+#gm!F{Lrt*J7Gz@cyp@l&ok&T-UWD(CuVp@oPs_%y^f8=#NkLk9+W+BiGMnJ^%C} zeh3(NO~4@a5Eb9wQ|Hj^WGI zDPi}L-RdEsUhoVJUszqv%{WQ8va_&uo)e4oE(*)i$Y8sehemz&=}NZFeS4Yw+=stT zZ~mOr#_!HjFz@8!)_iJ+$b*E>$tGvW@)g-bkBT>r%9HEkk?UfdXwn{b)EY8?JtitC ziG%$l32=+KM&pot!!*$0Ak32K&yi3qnjxCYjk~@oB0?YM`^_L?3-dh!l_%*obtCjR z{bW63CWo}r5?0`#^rZ~sx`DO4R>o^NC?Y|D3itSXOtt)io z8nsMC1p7><$+-lF*5(6YG)5>whr2&{QB5D&@Dz-@*T;T^Agn^s39s6g?&s}Nh~g*s z*tF@e%Avxl{<#_5|J0XF?2R}Z zde9HYExo62Gl~{9jmKzqQ@jnmmo;^Bq0b5Wo>f$P zNi6~zs+AI}bEQ8nfqvfmu!VB*DdBx+(L1Y)qT9L?@5)oD8Ldy}QX;1!HG$u%wP-A8 z3_qP;PKSusEnzo4Ln7}i2oL?B{M4*_jz*y}4A>h1Hpl9Gf^%pI7yMSbH9KcTa&#F_~T0#`c&6FUf{Ja z&E$(}QH>v8QFyy=d2GkCIgkIif|#$k13bg^e-QQ6VNHK=+k`M_kE5ICKsJO1(T; z94_A~kwn3Gl*ht_RcCbLe1^Z4?A+XxtID1=Ly@oP1bFULJ(u|{p40WsT)h*QZ{J}* z+phfjX7q+ettBu@>7JS6QVSu#z*)jL+^}a_09cQ0l$6ThRg>$%zLb0D$O}2WS5MNA zFYwt3H(|{4JV>E@*lXWY3%WQK#a`byzw16C#5R#=-JfhGpl>@N^bjw^3|khZ#Nuo5 zJ1S(FDDv`Ja#-;vMEq^(~Y>M@YS($;wq_&lBh8r7%$%l#uK_@*jnV|m)i30Z8y_x8_h&nU+{ z;#OrY<3Lv4msG!Qq-$i>v)|50KUcApocUD2;UaL5Z1hrDpKp`cjl1dG zYG$F{0p}YF2M0+i3i&pq_k^g+zT7)9d1$6mC&OA2iJ-I)_-0mnZn1_7mkh-rFHLSx z?6Jg=*z;z`(I6U5se)47_7fS><1&ggZ=2)7Pk(7HiqVk<*!QM0Be2KYE3m4ef^U_X z5x0g>-nfp#kwq?j%xI+joW@;O*mAJ21QLkX`tKyDvOk%v9A zh)tc7javsUxX$W5JLJ!_qJpt;(3dyg^n_iSeLmBF4q*n0#C*KY&QSNWH+T-oHpoNT zJWX?S5mKu5zj0$XsKyv#G3tO|%^Zz}_IIDei|VJD#k+O=z~b4 z)`I#sAGjWR`)~vf4=aQHaWYAs-Lp;W=SKndT*HLA^gK6o_eA(XGnhP?gwQA;WL;GX?UkOrV8o zA1|OP4}U1iSfYr5n(K%m2Dge?^ZMUvwdk9DYOo&=l0%QBXD1kI0!{+$1oK?Z??5^i zS!ZCThEb~c93ApkQNMKGtAAo9?I%;&6P9yf{SnW53r(B@}=)Zgv!0VR6nTg5+1q0d~D3mzEauF{&-{T z4;5OWyy5Z9_IzXPy-kPTcJ$bhNh9c!iUw$Dtdp zZ_W(yAnJpJG7i{rE{c{2iw8Hoqs(cpt@0UTE0M4$_+_oBPUTZ*OcqG?xKx)(!B}d*eB0Npf2DY+`QC?? z_P(=ODj*-56Dl`g0v%E)9uW8;c`Y9?{FMqCQ5TZZg^9nhDGvg5K{s}aX~p(PHu<34LI>VBn1j6vsuc6Kjh zvBh*7yx*i7`v|8em&4~OQ(xRKJqUHGvYI=h2CSYX%@djo8pc3DbS+EVXyWz;YdA=a zFZFnS*DYr1xqn3J-BPaV7X`o; zryJa8^Re!F8-aJ3Ju*Y`4)61j0O1O#eeY{|y(%Ylo-8wfhT-AodFnvW@5;1`tAH<) z4ZPV8V+2iAR@Bgz6LYbf71<^+ZO0|e&!@`m%#S+Y?_?wHT$r)pG%VCO)}qSgp1Nt0 zx=RK+wz`6eDcUGuyJq#1WXW>PYR&}PxKDS5(woX+%}QvD?G-bCt6Z`Bj@Vg;`N3}Z z2bz+5gT?(7ZIf9caSf)xQQLql!w_?8GS}1JgNYRUEmu8y& zZ_Sa6uMo$g9pUlRJG?64!xN3V-^i{gFk`ydTcR-%X_TwgrKll7yId5IDjN)1AfRCo z{wao#4|YNYi;Bj^khJ+#L)OKHP3oyz8&6=Puh{ITK!LZB+Q>FdTc3G?ru!IjgfE0o zKVJTr2xMwkYQS^&laLj`8Ydceo6uoZ>sxHnNPo0dO}dtES~5*9UIC19xNUg%9Nt<^ zveK&^AQonv78Og=Xgc$Yz(XK2o^vE|LSQ7QtldHLODCYPt`_S#b%9y?74xZg72FEW zFL*Jn|5(oFA=kjon!3991|DC*@l)~)=6Gj~W8GuFXY%HN_!`!wS&m>_@4WEvz*z#p z*t+`-;xM$>_xFe>e96+ap#CFSVy%hupAIhI7Akt+r=8th+9iaBut}gg=S>t*CuM^# zVz3nc>9bX*f3)H#K7P}Y+7b`K-b1OeN3)7XujAHsUI5q1Su(#76-m^Y71T}$pqs;p zh>j1Jbf>r)hos~vv61!z`YqGo3}Q4#&-k9m%Z&VZuT~y8%wNlMx(!V`Tl9PHut4+e zo56TZ2U)Cx@Wl178e)>_Hhi@+zTTkmN<0*~oA z_{ep)ADi9Mv-z#=Z;ib{&jp+es$pXRb*&+1~54~S6 zeKweaP^2BR+riNxEJqgTIRH-FEmUCM8wqs&A>67saQobz?i`^b+~j=r#Txeqy)$>e zf%vF|%Gsm*z~RezbR7xs(OeLO!URe9<{3ikp7V)GJ$>M+xq^%#dnN#EI+$pcCTc;e zZJla%lI=*VD~iKAQPI1*-IN3dV*_uS>hqx_&&l z36p8%J2GMvX3Q)Oe%!{s?LUL*&D>|peVqxA*O_a3r6p_r}U?j%DZ&ku)lt-fS$ zRdH;`UtlT{ZWh{5*WS8SIpI06S(XyRO~h)uw%EHXoH7zN@n10VV|rRdRlQ`w&Pt4~ z_9ep7XWb9SycF?}F>>dr0Ut><6ui{`L1un*b{_#+dRo6h2bt4kesD#4dsRKokQ-?~ zK7ZN^QZQS*8eyK3AU4wX>aWDJaNVz`C`cRi7^$|~n`9+e{L%hd%ERdJZC2Fu5cnk* zD`C9&Tgq75er;O6j|g!AS}g=wnpw3bH}CD2VaW^Jk0i&_<+tT=8b~Gu zvF;z5WqfDbHX@o71YVIgV-LR4Lw^#9TJleOTrhp z@J`$XC){Q0*SbL%T%l$n6>|9^M2016r9!X%QANeth(fVW=TkH~_6h@FEWCAJD!0Oi zg)|^O4UOJ@3&!BNRHb>i?NxVrI`ZB#xN-rDEGj7z?Y?#*Tf!p*GukVmiz{SP+(gx0 z>vx`dA!A#^MBIqgjwvFRd#;$!F!>P%Dz29jd(g+(CK0`{dVPikkWkb~N}sy8W{gX> zVr*JMow-t9hsFAqMn6s!u85nHa2_O`{q4N;6&WtvEFZ2<<>vGERyfN2vuxefVl9g5I_eWWA7f;9HKS6B9 zK)0<7$J32rcr}u3k-Y7q?2&Vja3*;;)QjMZL~pXgAG-U-(^m}{&H?Y#B>ZAiWA7HM zaQVO0!L&rE=C9Ep{b1L?&corXwXkUqbM-y~<$tRb9^UeZ>i>TX81ZK=9X~tPf|8HQ z!s>rZ!)TFgcQv%?oau3Uvj_FJO<4?j<%FMD?>L#Vfkdhb8x2&~izU-HUh7a7@itn* z0&4ha(7$DWsN)`9_9I@oIl$G^&gbmGGd6)$%nMzYYkJnVoeK= zhea#jG|q-c=mHq|RPtH+p3wYtJmhZqE_C%@t{Jn`_{Z5Wvi1?%wB4SAXvp0A33po} z(Xi9=VDa0yA5+qOD!LE zZ$^|784`VPgf*NW&Vj?QIFNbGoy0w4{Y3wzvbnyfTn-E_>yM{sL?XtY6Jc9p5#8d@ zO4V-$JT>RD?)zOnQY<&WPa`H2&;bOhyKe3~O0sM7w)Cfeafwomd1aFY>Jg!)1MSOh zJ6hw5D$tk_R&AaO==+lvr)TExgYe^I0_=!9Y;9p#o<7V3hD+UXq8VSL{ExjVXiFA& z!Vk4AOt)_bmIb63&-Kh?PS9@B%jXoV zY%8Jmh1$bsw)ZgL`Fh?3rF}P9z(v6eT6>xEcbb zRnK)?z4fZuc1e6*4@Fm36Abz3O-vm0(A1Sd-{jowTW-}_ql*c^01`mv!PC4kN1&lG zYnK%%)9Zg5EXs7NdV@e0qCs+zYtG2nu(3Cs>nfeQ4EsPusJ(%T-w6GXjqpX%NjbSMPubk(nG$lb zmEq4nmYD2&EZ)A97uA<@U8_lReYsd1_hZnSuMpgjjP=C{|L5ylTvf_W7?Bs>w9aZU zZ$Ze9jjFDp2ZHWiN0UH^PRgyzuD+!F`L<0kW`(ZhS2rAb=JRCF?6KeaF7MFA z+PcF5B}?0@t!8%6gi8h#6_EfbH~gp;gh$8Kw~sxCO@X|p#{1&RyLz0~2H zuDr~%7TcdoP%~bz=ZYwp8>9fpetX5lp-q$(_!O^7p|s9M>^`qgi^c%qE6VHqK3`@^ zK=7!*Hmhesq+5(xjdsi0cWSQQOOg@wz~o4dx6oi0cXeb?V0@&n+vI_zUAau@Q~3b3pfzP0D7L)&;_1f>fEW3} zII|5^d5$im7=rUWWyG=At@F<)ZTokfSf1{or&%hXG3?$U-l4}{O_mL`jRrl(ZJ`?w zNs<4|1p+*m&C(w?XZH8P!?T9Cf1~?mqkFoV!WUdOglA79_Ll(*r7mWqTS9r)M(Rjzk(JOPT z%adO-;75GyvPOz`>?CVdEN)GrT3w>;C_EOt$M7)KxM6GV6E<#12c^rJJs_)0D?9Y> zK=m;|ZFuia*)U6cW}HN;9LAs*;^!ad9JO&9GQNECK?8@=(JeH{YP#Zp50|1B>XE12 zF5t~t8eq?2#<@n51+RA5{O6JA{>=(?!L>5pR)+SoSwb4-zD6&jkVrd)V`|O8jTttLX@4`&z2LCCs(L>wMEA=<8WzP+2NyCXcWfHI7t?2kPpS@nc ze7UMZ?sTLnyNbC#-cAqwFh3z>ma6M9 zg9xF>Mzl()8GyE0iDSxz6+itEY^H|R>3n5Ahx_v@1}G4cZ#N|d4M00+V)uLrsw6TQ zf8Sci7dQ_Ge4Zef|s-phrs`PX*U15JQq3mL1739b;&&b$1M6=dfopC}~ zkX{aHl6I9jN@i!S!?~d8!omSl^)flY&g5?zGUxp9zx=Cyg=2Z-YdYeZ>SynKABoD? z33xD;{GqkzV|M-FCS+eil!K;b>^lj8Yn%pc*Xwj9`p}A^ZGNvDqEwDEhH!;Sk9_Xv z+m3IJqQys*UU#QG*=P+~CjCceziSW-zwycnZ;;7eNn&0jTBC#Kc4`cB5+SERi~CLr44_PQ$1lv2VYmq(_wmjLKIYXFa2#i7p({DKK8v zf3L1Qt)z5iI!wT@PI>vfVwVZX2W_e0N|BT#8{6)TpUoOEyFVk^B5ZmRS=(=M-0*zv zUai+dh^7bFg55l}8t3_G{!_&5ZTHiMVf8YN)2&f8c7gYr}=3YZx=7Tv89! zNCH^q>F=U)OQw5{2Ca2d{H-5b_FH27XAZc#ju2ycP53?FtKF|dLjrR_T-$EhmkEE? zp`odp2jeyB`RuA^y`MolXJSe;J#S+xLSygX?`e7jJ?HyvlkSOqZ+ln{e4w#bWD4;l z>yMudg~GtC;CY33OW2BeS6fpJ3Ltk1htAy$;^*5Xh3R&>mVSB11EV(zW&w4CH_FHL zIh;U(HMu0C`ZqaY{HlY8oL!)T#*GdhuJvEjzct3cOG1%{9FTkXd&4h{q@cfTSVgGa zH%spJ%1bU*%HJtKu(($X4z@VD78}*OvC`g6v+q&J!KaYZ{X686z;lj{c?w7UJfvV3 zz?~3(odk#~jNN{Z2` z;DX5B_o;1#tZ22DlE5gH%Zaidkc!pNx~A7t8?nUb(%a>v;yqHG6e{rqVkr3|As+1{gf#>+3US8<|wnQ@LgP)gj|KIx#nr(S8hcpf9fftqamr}2`8 zk4@lpC`P`Ih=*t1C|5b%IbZB85R12X{yQ}j1pycYvcM)SX42_R*+-Z?x&8IZcA^YNyG%FQq)!z_ z)Tz>Y@y(r>h9tK3F7qcw<-gI@eB*o(R$5w#;|2XJO{hfoiIX$DiAHr0-#{^5Yiv%f z&VTy;)=t`!{|$|P82N{{P|D9b*ES=u_)8R30|N5@h7U=ebtPBUk&5O(aYD~2UUub( zX-Z;Cp601`S@JV9sM7F3ZtQgJTDkM-n?wXYUI&%R)>8OW^qb087-hM!k%ps(*9I#OE#9?yExdN?tO5vZ$VXZj$*z<1|N2b6`@^=j{S| z$aRM(wZXB^6QjlosgEJI>DsO}Yiz8rrKzw1Z7(gpLn9ajg-Sd+Qoc*r zKh}VZ07BYlKt5L?+W#BLtMgW%LF|lj^eJLjIk;@_%xpnKkapYuL*@&FXip1YL!LtW z8PkMKdXffcPJ<76gr!>WII_$mh}qb4hhVSHDwq$wi{FEJ{jw&y4bX^K5;taj{V|FJ zNB^>c+8W}g@T$U*1d_VTj@#i0Y@vuX@lo5NdW_8__*cg(3tCpoo*o&w!l89kJW76p%T>ouyLZC5Fv9 zczF3hEe2F&kxt5yjwZ~QI@+cpULogLQ@Ry-Wd+y#@WoVE=-!NEUo83SdW#`n!lJa9 zNkw>PDi!hMd824EQF3!;48k|hb6{8g9|z69BT%a+HZD*i9_it z&sTA@(bYfOyHR@gJmHHO9`-L>^t6y{Dx0f_zD=E8^Eo-5rg{)Qeba059_*N%N~qhK~H(toShhdQ&!Gp}{Ay7q5nll18+57FD}s#n^O zZ!hO~|2sc~$vsayXg?Y8ZATl{Bhcs!=3O0Z0EAz$t03_dA7=c%d?J;)Bju$R5vOjj z{VAwrT9AD_bZRyT<}%crKq9b3G2q7REBifhVo`JEQ)Q~`(GxDD5niM{G%2_0L36hm$(<`jv^oC7xuj`Y5@^B}@0H;wG0|F|&}0ipZWP{RJR zpSfw%6=7Htd>YGHz^;dGG++KI^LgW_l#id>al2e0e^W!upMI~st%gR^d?0V-whJM; z!((=-bTd1XVbVsHAwVzT(N9EtohL;7T;JgCA>DvvEN zxF(QNmhInFeCj1Uyf#ED^E#ihB}_iWiZ8M3F+L4ufhJk$TA)ZkqJmD6Vs-arm+T#TU}<^)DGQzxvfpmvh3;>4!;>1B4r6h z{MSG4ZwMK_gU<5>^Q$t;ulBnkE}&hi1*RFxu{1fu@Z4CbWlP)^j|Rkpo)q!5>c5*g zs%MKkmmWxy^iibT3^YErhAqcBfVT< zlqyD{w^Q$Lu_N^6+L6UCAzw0%=|DIr=&D%{!z zqo;fQORw0lZuuj9K}k$jR9$B&T*s1R8T+{G{m>bx6h&z}%AQ*J`8Cjqk5$Dvd6IF;2@s zlK@eSlUQ&1lUN7|$m`mCA=uOM+U(L~b+b`jVRN#G3D$h&XUMG3n>pOtI*_{=L2;N~2>Ez?9qyoHdkua^h6h)Ctt( zqgOZdW5Oo%dHOD_GYGCW(ly?puXRqk^!JYgCI1Fi+ut2`@6_8xn;BV@vC7pE9=FfF ztV)xO5GTrs&bs66lY8vt@B`;Iium?=l}r!bm3qmws0Xi&p9eW`5ZkZuTxU`+M|xZi zV+9HNbh{r86m>>nnb3Be3~+(x<~42H3L=N}MvtY1v*l7s3D=@x zoxEbga4LvTQOQn{0%;=-vSS>I)fXL9* zggx9hGv}nQF}iKQGl@$9F3!0mk&gyqdTqWw?rr&?7ba*MPWZ(3YdgA7;Z*ot2Uj1< z*bh^fGCy}sEzaNeO|-sVjBlleNX{W5Al5XB;PuJ1*`IJ>p=%;8Q6>-<1ERzw-MhOJBufPs)UQDx zj4(a8zk@ooQo;w%VpW)k%Mit$lAyDE5Vk(V!?g)Rr^ugPGCAF-TznRA9Z z(5vFKi^i$10tc(ft_3gDvmCj34)F+h4G@=4S&QbvbDb{6K!HqHj&J5d*^6c>9u3#T zfjL)^h~N9D`PI!rU}~bJJXD^6&mJz#h?c=b!?kky40}ZcUvTLhGx2vK@yT!xQ>9p} zdp0!y1R3-zPwAKD3$Oh%*sKc zv7P(Be=MC#m}PG89PIF&YhEx)(6bCizK@HS|9FRYIy%rl5gO|(X`i)-LuV(roRNm% zUdpE#rdYg8TMdcH^HKciik%Lae&6K(;#SxQp<7lp&%Uv=;OO-t%5u(PyvU#+*J9|P z+V;X|jnYKx*ZI~KQ0B3`9+m{kSiabHsJDWOT}riv?!uXdj_?IQV|0GnsV>aFRaczoct!8hazXQy=Z;+EK7NMvy7m(BXfMN`p(X7+mu zX~T}dbT08);4J)YQNqHbrIMVL zsd~p>>JG|#>{ySS9!wCv-0${U#424Z&w8`Nc(XgxYtrQI{pjbHNW1>Xy!%q2vCkQj z`EJ1Yu3cbz%wNbo?dSwpG^iG>cE+h2;TOIZ#@m`(;J^VkD0o&D`V^^Yt|)VGT$F!Z zBJMIn)jr;+u3o2vuiYI^+u6^?NoRl?9e8+lH@uRF>EeUD=#HOEEZ<0MGQBlHPFNR^ zM42CQxtwpZxK!jshqe}&o{Lu7d6|uaR!K9#CzOb3C-<%=rOV@7p&QST{XOqXyfv|K zAoJfs=&<-eDHJA%!gZ7A)7!B(NK-8w0nm4&&>Z6i>T4M|Pxx#GSD=>9^FpCW#lCUS z-)O&Drj0&j=l(zg;A3!>v%>YDu_44vr^ZiHUpv@baFzck=zx=>k0W&{HUoP#WTg>` zSCn%RJJx@FNwJwobJ8^u-yT`c_txnAv~i9AKuzx0?F20A{q`<>W5PXrZO7n5E++_N z<|GjQ7IT8%MLwRBGT=J4>CdBs%qW)uS5U3zIB~) zH5-#qatjcAJ0#q!E7$MzCgp?6`Hkm5DG9c5PW(Y-6L5AF!t~HO9Da_6SsVaW|6cJx zIJ@F4$(g1X*5WE0(SnjT?#9G5j?P zon3*YgaaS@>#MvgDRh3@dfj2;R?zBvaujzR*Y?vA$Lk|dl|8{0qaW|EzgQEuMg2K{ zHC9Z-MoTjlPWC8(j-B3oDwtjQv34hfwR(o9x~C1h?hVowU8M#Y)7Vmz zeB|L(rG-p0VZ{UfDW!1|{Z-^scR=Cz<{WE?*X;=*wpZ8e_>{t;Egl?_-Y_W_Z zJqAKHxSLJJXZue`)p%5Fg>GpZVLGQL@32-aLjI|l6T6?0X&wK8-zbfLgZ^_^GZ#EI zfc&ev6uuQuLvE%$Az4dDgldpZ``K)NX7`rfdzKlw78AL0hVuPhhZp!yETzI^Z%+}TAnz2g!q@N=r=<;(RL;8XH7?!;LMzyu!`loD%*Xf zEehL8*HS4)sGr-D4&AKK#BPkO9g3cB!oa`H_jW3~f~%jRA*jP!{$fC4S~Ur~5aBpv*+^kZb%tVO_?Zcn5QNh*_n0 zl(d}tY3~8hti7GSOb=>!adkksr5k7y<{x`GA)qTRH=rzvH6||br09DNME&+>QC2*G z0?N)N2MwxieH?~6#K;#4L@XCN)hW3*rnQh#xw2`ayj!a*e%rp5 z#M$!qV%upud&_8L7(`KlX)LtslzG?x-opYot0D&89&55hJ@vRp+8gTKvi7YmK1;jB zRf+aBwL?`>PG2a-NJvtp3F3w*JhqJsu$?Mr`61RlJ>__Z!k0g5wj1{<+y~zzJw93W z_z}dA&Uff(e}OOj#8>wp%^Bh@hcQ1&;4gA_|GuwyGnF8;{^%63qr%-obF9`SrT|9s zI|$}m+Y`vjB?C-am*c2eNV{0806ByZn(SSZE+>MJ6H0-NoBKCQX`b{Q&DFdq(2M8B zw@z%1^_%ci1;-s~q0NeyO(mW@eW+cA%*hi&vAcz$2*vlotIAID^9jpqGBJJ^ABVlS zKO7$U@Ki{00LQ>>K;>4jRPbtOAUrN+)W~&_PN!f7pkrox07RVH)m2dhd`1d8@rWN0 zL8d$?B@W2}PYjtknPDkh@*D6HzxDU=VL)wJJ^wxL#oDGK_qLgXB*6chJ#+>wBCqzZ zF=*mxE(;6lRB>5cJIgJA$|*ed%UR zCeh37-y{w)5o$xdcp7YGd)+Pn{z)RH9(7`9cg3sVeQL@wDhw-(f1QObJQa*6W^jev@JpiE;O34^)A?p0~B%3As@~jQmsZ zcYV^CB3I`_I6{*-PT9>1gTEDXaKz4wIlts{gKN(gSAo$)iR$Y+rFTbI`D^_vi3&&erCjN0zsn4gNO{S&=9K6Py3+aYrSF$KQ!^9l?~f+uCvVx+6hHj;nFiv@p~g-6RbWNQ29 zskj=Rf!Yr*t6PtTqG8y$7`Ma{^Vp1d`*L+E6g#aYSa* z<_NW*l%op5l$M0<(`Ar!p3tann>SB`)8db9H5rvU$&_S_Bqq!Z0W3^Y(pY1=zlHC| z6keMC`Di+1jhkYo9`2^G+)7Jia`T-21j-}fy>1=jeGG{GsIF-Bh#LsOhwJA-tgCUpem| z|B`J!e*eHsh)J*MJXeM8FjF?c$3zABJg3A&!B<0GpeC{yJ|xGWt)zZL@Vzh*6Kl(A z$h(*$>PenvU{CC}iCRzQhg-8_f>_%N``}P6n`^p=%8H6mDw9!J=tnAO>p5Lp^$U_# zf%0zAu~=B$5$7;1FZPMbO2pBfTa_7HOAOUlnPy-M*nx)AWZ%%?fw#?XY=E0Pl`|8H zfw4ND|A7Sa+<(29l!#>NZ1C|?q_F!=gt8_R8LcIrTS)3xUTU=6Y(VC3iamF1jZ$(K zfkA!;3^-iM49YEakoK&~z*YK4msk(oHXIj z3VE5GI#bLXuL9H$1oq||UivmVKlPemWZ%-Po{r-eSCi{J;Orh(@HM|hsdW7ye2l## zs-I>h4aZqst=2DRW@YVe^EvNr&rZVbh<|fjsiiN24!&0FfB`eTs|JbQr=%nh-JQOR z?ISJ*TBiblPWhVdbs@p8r!5$A_Jm9qt;<~RWXb9-OdD%RbL~|~J5?is0f2^+-fpXm zD$_=iR3m?W&Prt~fAlmYI#*7T{5omVr?pR+xy2lh^$ms#+RiRHQW_qLM-c@WSHne0 zIA{3=h6FR2_^wnD&o$x9HAinW6Pt^e;%$=HALV|2@ z2Gk!_-&%D%LgBic_ype%Y5p3f{P)WI7to=~f5i<&79m&u1aGpvy4WUOuGP7GqQRQy zm$h>!oc>LA1!sOP2f{~lYsxZoBy9*T!X5i7Mx63A)9CUG>Z&S_v@uo?m25iWR2LgCyS3@_ywS;3O*@FMd4 zJ|00Op{wFK!N*GSfqXar^4`nbG+fsZIgr&P^oHVh5eIBzSk~}%ps*OkiLsCsnXtTp z&UE>v9?Q73MGukKAmbYB`Z2)%yO7UK~^Nzu%iG}ja-}E7tOLCq={<}VR%x7z5RyZm(D_#$=EkeO%6zZ=B7{87&_84CS3A3nud^GSO3)PJ| zA;2c;+~309F@Prt3E89wbP+2YtsPPrBmaac)Zr%PLA}5DMjx~J^%Kq40Q=WopdrFE&^72dHKZe4jsH* zuq1hWXtmmiYE#R)&{I=%PzURDiRvO(C#&&Tc;pczOG!WpmD_!(0!DtiZ;_2m^mo~N zw9^S(fSET#+65M^GadqK<2h)TS{heP%eoD29DioZLvAna@%))vxo^{T67CS2O{IFI zzaZqjH`-=%mkblf%b@osMH7_3#4e_K&^eC7}){0*AR4hFzI4MBZre_tEaTE_Z~Mr$i__P1#we z2wyP`ujJe*=O8aX@~aDYm=r9aZRxP@Rv>!-YL}({&@~rk;0c(@DV>+P)itOUSo!Fbs z*u~ZQEBsS|)Hm9Ajf)H`rX(aZs#&}igHh8+hbs(HGV(~mGgt)C>ja)28CZ?l=0b$B zlDvNG6WJm!DTQMP{B9@S7%gd$?fvsnTd-b@(t$+m;}nqDC|X-{dYqga(Syv zc{{0|CU(iC@^I;n-+Jf6z8ULx1oA%Y*hNgM|K_n8l3))w|0H$^^P&{=VU?hgB*_2+ zdRy1X0*DAH3kwThbT*vYpdrSx2``Bz{dBLN|=4z~>t%aMbwPY7trlP|;AM*(* zobkW!hm9>Y`TwNA!Lg4H4IONCwOTgNtZv;T)DOfG31B@_z2m%#sQDPj z)}U}xz2!x;(~J(tiBt$?-8M0fykZUc<8JADI?Po^D>0jbRVe6x0@I3maJBxK@#`Fd zXgjOUxO=f1+S=nnG}qTc>3~Ml;LYviCzZLC7Ruj*kHoT!F?2Sh@cR3HY%HnMn|sJf zUhFalvl3K9Q^|fBnVQ4X=Tl{O>AGq;Eyh(->Glw+_907!w%+GgKCk;j7-&&|V_#)D z%9BQbK|lmsyah~}nz7$Tk+sk$tp-IW|Gn4r%38LcY$H1F+I+42dh^!cxIY#%%zUdJ zw9yRqNXePP&@uF4c`I-qO5mVvhRmV)@9|;P2#pjLVz&qseu_l1S?a-7TDEbdg=o_bNz06l*7`$q=1yfz z*s|N^!eHn`6{xOQtB(nV?12!Gz{AnAg3nt=U(XdA;DF*j<}9q#Sq5iFYlm2&Ou?oAptKEP~BKaa}tEx-N zi2X*n&7$kh9zn@~SRN32Siya0le-2*8Ra#Z#69?x{ND?b2F(n*+nlJAj(d_Je}Km3 zmGmBJ0$+uFuUw@?I%w@@Iw&=Bh7qD~Cw~vy>i>@U@OIetQ%|rlOIL~vHOq~^x65~ud)wDn zM|>2ceygDr4`&-})P*Z|pQw&A6acQRIJHb)*R`p60_q0B#~wU3M5Ai<7u#*`O7Orn zv&^UKGdybS!)NO^*F!Gnq>|9|eVIlt!!+gav_e0W%LkWRa4C@R9rrpKEd_yN=0>l( zL*RVUFPB9xdQ{b9h^R4Z&(I=@dFi+xcVSki?8=NHpiq&`p;swkvr7V4LAwuA!lj-_ zrnA~tfl)hkv#^kRe7BkiN>PmOBYil#cX{lJQ1!^4?RSpJ+U;QgrI-zP06V^GKD@{M zJPX;BW}G%?f``&bx^9MklYpo&Tt~8%w3A9o1dK+i4-LPMAT*thx1wzMx^w5H@66Xi zUppM$Z^8xt;J{LRlE_5vCMG(P!S263niOK7A+h!|W!J zEKOkj-O~LH=SAC&^^&(S9qHI=usB3w{2n*+^wfq~&fO!@DaL&~caH z^(z5A?S%L=L>FZqmqbQ`mF&|gI&lks%NJ0A&%yj&OzK?XK&w?KNcB7MO7eZai_Qm< zmf6~k<%~n`zu2NH4(d$18JM{+j+BXcw~*39WPT?d$jDNTSL8KvZ!P;dya@3GtNt@3 z$wrp3im)l%%WM90hXV{Dr|I&@q0#}@c7MJjl(Jz=}#iBoX;2ujK(7xNaW z56~gKEzFoG4#7%g`sT9TEKpZ_yH3!cOL#fvP`{Som(N{OL1ZI#(Caq2zk7_0#4>7- z0qXy4lw}X!8B5LEe6sc+t+}UG@GW7Zh}^N+2j`w9gCV~4bgL#7eZgcDabypCo^t_rLniRgH5|WADP+ zwPwvVr=IBjd`rV?z4i^x7{KPFE*=6nm&}}}mFkpBt44#)O-Z4s+N{Hc$6^%6f;8`8Cdia*Pa-*>9)?yIp_dDJluYaZ&5{~Ih^~8Tdo9}!u_W({D(u<}l z^G#S%VHeH<<$T=0a`om`CH*!YCloB}Ex3dT?&-Nka-)2%dGTFUzl*9cyl~eJoufdy zY?hfOot^uU_aAt;;cOzLjW?51gmB{om3Wk6Y6)g%qKHe&C800G?&SWUqW7Nsjg>O@ z=L=S|#e{Z-KYhJS22}#7p@CmhF1S_gyG@KIKH9VB1Y$sVX5O_vy+-bqgaBzbt50M= zSCk@fo;)<%l_Eo6R+)Q*pgbgBZ-*$Dghfs1%3-#rN^CV!ue8Kps3FC$t%+UtVYVdS?T6jaM1|ZQ&2R#4!5eUktGbKqfgt z<9}08zFe7!t%k&~wHor74dc@35xV%=jPp1dDnqWO5fUxB%60ul5AlhyW9)O+i;3cF z=T&Wt7PI0qS{*C25*6^c`)G653hg&_M33Pp3g%OfcT!C46vBVWihpH0^xqFc?Ew`L zXYcoHoK!CHPTp?1J)KwdJmwL=`u&OJ%9 zb)JUpG+m48rYQEhpsxmQj6L>lrNd2r_)M0$&>J4 zWpPNd**SjmLB2`4^jolgYy-^0bTp+qVxXjD6g+106nRahmir|I=Osoc(uwW_aaZOJ z&%f4tK+La^cSM?S5Xi#ghcYW^a1J2rjN{Q0?x_97dZ(m_rHhx-vp;b6=6^)$imkn4^Zwv$4WxRAAo+=91uvAeD*+sq(-YB%<0msye`$g zV5B2t7##B+kTeo{s;k>|I6Ta#0>CFZZ%P z-c2Z0>l)yH7grY6<&7%6X6r#yZ*IEy@nJ&Jz3bbIP z$6)|~jFnZW?RIvd#Z1jfn~#YreCo(jfmV^{#X_QMQ@y$SraV61`Z!%~G$QVAaBH>Q zdP|hUVcx|}Lo)Nu$x^j$bAZ9HF!kQY@Bc)wMzB2`bGtSuf%A-8nsFU)69HJ|o4DmJ zP(P={{|u6XwDMvRsm1y$AI}=Z@tHTtut^e3zF2iyrim7>c9^@yBkh!x`pa+c0k$~k z^UiG~1*3Z|*K~K*cLpuafm)*LBjKP-(0lNIDWe`R6UXt7USh`Djp?_Obn`tnUj zRIDpldbbmjSZvsBsR5nH74P@yM($L# z#St&&#g7yFzA1mMQt&tBs4w~xB|R8^C}R=`7S6jG4L#LJ7KBA=MlSBHl!Mz%`;y^l z99oO5x~e+KlbZ%8jZ=8Bi9$x_QY~Nv3D-XsdFClFu%pMz@_q4+B9=R$IEWQ;cU5RA z*e*3s`96u=qoLcE1TGZ@K`uYE^vQr;&vW{l&I0|g*n~wwsIW>jS%sfxffO1p{y?Ta z(MNsxPJ6KC&7h-V3kRJg44|<7`5-gDyeII8#`S4tEO;=Mh~s*AvZ}4BMP{S*c{s~a zL6hD(@grthKSO^zyz$$%o&yxp2kKtk|^u zsv{HofUk4qhLP*?a&j_7E)NYY)1Ik}`$+^&?EA_y;?ISIka9fU=i5H7)U#($jV1r; z^PN52RF=ees>>X{s4{+Km1RU&;rVWv0CSo1z3YgHx!QA8@k-yyo#0Pj2*J#l@JevN zQ$y8(fv5pND(AAML>BIY;oLnZDz?VAFZ?vWqwK)X8} zD@X64U3zU_D&BJ170jVCnk3=1I%-hA%gs5wp;Uo0<5#`$7OhX$A&jIZyc(5l6s zC)Va5?`u1_{SM-0VnVuHYepZxymlOt?s1s+Z6Xx0`1~qHAXFf)8b8~YPUIyVruw8U z6mvS*QE|0n?#&4gAC-pNiuh>iPOyS;^^2A2VYUj7&Y2pbqYiTW*;d-tcOqblLz;#zKYH2?W{ zC%E0oZp2stqSqbr7ue?i5g=$|#%zTS+qJs1gEqEU^ra{w9a> z*emz9p6!f(TSFsHdsJ#vE4SL~Pda;kclBL)j1>J%O)umD-RT27dw5A-&Xta%!gqaB z4TdED1IUTR;(5GWvrK=ZUTzF9?6!v53gm%erzo`QN#CjIC6`J>ermp@}Wj&*zd?t$ZGeaJr- zD>aoyzaXUV^O&L6+Fg#x=;`HZlh0+9l%%Kg1j)OiCK5Il$8VSC;zsXo?<-=F&8DE* zPN(LTc>-viZMekWLpO0c$q|>`LHsk4NeSKIyvKd_43(+^=(|UaR*damc+Muk!Cmrd;T~_+xYzt-9RG~@}zj=Nibp=TM z$!-t5vl?{VlM@FtGS1~x>*=E(*Xo4s?GYdZBD8ca;NBrL1GK|c@mNyYo4ENx>vyw} zZ+v9zIQII^JrZlx5C)|4KVK4d2oag$zl=DUT6_g25YBNB0CsiEf;!_ z_LQTWw?D$S(=3t-ae5WAL#;ZaEB0%mAI+-)jmhSuGj)XC9_Nunt|s$~-0;tOf@nUg zHLv%5Qai9ya#oo!0hxAvaSd~9DRpgo)V}qkZMOfi_t9w-Tvfbu{!li6S#bH|e(_V) z9CIt-M=FXhQ-zlWcH5`mtUp!_I*=drNv;b*8?>HaSL6G4r=bexa_}{`TB8W*AiWhm zh1LuPQu7cCB-novGVM$XOx948#-OGo$`w)#9@?Ufwnhl2}VM|9QRIdguRXS@%7 zB;E99J^w7O>6pmjRh!UEAQBW?A{=kFS;i{9p3LTsNp#}{HJ!>2s%B&Y1kU3XYD{uu z&8f7ylQGZ?TqJIr^wbjZQD66!t)H9xU`FrB@CRAepGgc(9r?4(>ib}*rHvKRLpWdO zFlWnvXbT`F4hUX0$1Kpje$#pwVD~%(NNRPN;8Lm7)Q=<^U}61_X^o|$2TZ_WD>TEesiCmu!W9NN@q2C#o3%gweuU>YfOGbO|G|w_p}OOA00mTaalmZU;&ROb#6rD}-G(8CWuHhh+A`=i4z2D1b9d z^Xr&%swJzG`NK-}w8N%vyHPm{0b!VZzpy`bI#AA+nFceSM@*jnac1zDp-CxBOJvWl z6>FSQb$M>uz4-1E0ec+z;kfLAE9b2oV?S}!x=wjYo!HzJfN;dXHgL|IM#txU;a`_W zyiHYKp~W(q9NWUUwSI#5<1YJ-tE<(h^xT=3y#-PxCE{P(ry*J2B82#W?(zQ_%Y*1XhIKwxduaTO$fF3^I9b~vjI!ko@+lg z1<}+01}AoUHPp(%H#Dio9Y@WoeJY28a9cquOsC94T5wr?!~Kpg6?(}b^9({pK zeQG^5I3IUvp0Diymb23N+v9^$4!mZQR$d|}O@w}cM=l&aWVuK#Roio3=k3wCU%kx* zm)`r{rB<`0;0yaX$AZ*&CG=jH^ix>tvIy%i6)_++Wa!{v2S2es@?S{_J>4 zx&Ji({S7#wb@SRqB`$rOl~F{5=mk5IjwZ)bafg=o;=g%?uia0iHW>O<%w2d}ja{s= zA~%>r^tZSQ!;E#7% zBdAsMDA{;Awaalo)I<*hZAOzY;DZ5wh*v--RS&Wo_@kD)mW^hf0}h_@Jr23V|5&+JF!V=C8q~ zs0Ii+-i^1A_uv0cse&};o~R$I)AnS_b$so1>;%$Orjpn*!vMK>PC`?J>vrR#B=uY! zb#GMztN|mUIe&G~XDJFXbIY}i)4~OPd@rsGOo!u_iaKRN zw?VLc*N}1iyD?>KKsTuHv_eigPz^c9O+L%b=tY_4r9z{LZ84#x{|X2RpnNkkLy!V+ zc1`_P<&URwhf9zDWw8DV$GMHzF9Y|qQYon=U)|kb*ARk(@#h=8U*1232*2(R+pfI7 zXJq;G2GJJ4KnV6Y4Q`k|@1H7nZR-6H?Yi6E^?VCPxR{ogLd5;e0VHaD!4bXt6yQmB zz}n7%gxvq|alhdd`={LJsOndB3<6MciCK?v4fOb)95C#Bkha%G(cOm*5XSA#oajaI z=Mr*VD8r7xdJ>nQa< zZ_w%oAh=CyI!U~=ZrvZ25h?K=Vw9Aqf6{dXBo(_&r(d7f=(Z|$+3+(mDi}@!C8(jz z?7ylSr_ef_PYU$(5Y)PlEYnd5JW|B_%ZB1s-X2wiSpx~=>>jM}LUPdfy<5j&;zK$1 zs_o^o8?lXlb-n@e6+GUn+7#7V7na=jt|z{N>`~+GAu4;jKo( zTb7fE$m{<7zVGAdvB40V-HmGpEf^NDHoq}n-Wjmtg79_14CGr4le@kQ%1DVn@ll~R zo&G{326BLNGPfZfQ!EX>COnE?>DKNHZhs9VFuw0DcXr$>fO0rp4Xjjj&b~G+{rKHk zIUOZ$NH=AZ94b(TnTyXYk|ZcnKNXkth+iPn07C^4?c`CHd?u;TJ;P^B1_srqv0 z!~?L?x~P5PGjDWIV-O*w&rjOy2>ZE$7V_s&ZMSwEH{EcHa0|Eic4|dCS+D$7?@j=;en3~~OlFcG|dc zK>YRRmR z>%EbGD!sq7d$8$yeX;hREYEs_JQh8>KF@(WYTk*Xy3syY??|1^wiFFzJqhnvST^k; z$^6g!8$1u<4F4()s6qduwewu@96kHG+}|Z8>E$waUksesYzH6aE<3t({F%Eo8EgpC zYL5%(LZ;xfj}s!q?>|}%A@FoE|6ZOy0Bcq=gv_DcQo6d)*g2(7*!Z21&%H)W8d4Im<+cFAWbuOkaqTl z6AExZXyIswGu-gubN+mRbcrFXyVFhc&Wmw5Tch2%9;#}muU|G3tsW6{l#=#m6NK)a zl?moI3eHcH+h=DMOCR*r(%k%(8UGd({9oz8x5oo?DEMmoO1@X1ISKT*X4UYwl(5nL zQZ6d;+F^!qvo=cP4gc3>vef=$Jw1wA8EMvH{P4i_h|$8s>&x2~HS72sf&$)>H2yr_ zhWj-2irA-5uOB?JUoY(A*|OBdQ*%b08Ln&*!;J;zugxO6I&PW%Kd1?NOO&M;FY>O= zzk@s6&I{YEi1S{o;MnB2cAGo%0dO|r95Vk#uv@p|q7JN^wNbN^!Z!^!Bal5L$Z3J3 zTFJQXce}(PrXRPI9o;OjDx9h+9WT1T>s(wK_A(MqRxBAOA_mW6%2>T4$(Rwu8L~o% zYhhuqZ3~tD%YeN%Y2gu7O$6&904c8!l!~jfXd$Ax9N|K(E-7?S_1(un%5Y=1={q5Bt< z@$H=Sbg~4SuII>Q{bxCF8XtJ-VmjeCgt-gYMc#etBpzn;5DGnq>WS3mU}2b~4GB~A zumWuu*K<5b?{BstUvh{N&s_mKFg+&c=Yw@iuR=-pZRE~yTH{PF@OYj&aqIut*5n1x zqvx+>NwNC=CuXL5-wJ1g5}n;$RpK$VsA?!GCv4l~VWC}%%Lf0>xwW|91P-CIicyi* z2PLnw2lK*r3xzP|qV-DjYfhTQd6f1ClFxWNYAr2jYpxI1)cGpn2lG{kca6eml-vj| zqpEVT3^)b$n)0C3<y6CZZu2;`>sZkmht46G=6lbr$bqzD+@zQSg#u!*zS|n-$qNqJXqeYL=NSK0Zto0X42+ zBKI@K;D2M8^2_;ymRXp1{wpg&+XdVBn^&H}9>AK$WEcmGbQnl{*=11dYp(4IN0#o6 zRoe1F-1cWBDB}e>zJk^96`>Ln$7E!vKlIqa*mst2G7u3L?d>O`U_<)Esok6$BNsvU z)`v^2-0kB4$v}t=)As41O6GfI#o**p;4||-0>?y_qCLNrENV?&I%vXcCVE{)yko zc7K7SDJn@fCGIkhzJuQatfI#_T@|d~3`8C~Y5J0Zxz|L{&DDf^>M5ppyM|r8@w|B2YV%uEb&IFi!=vPn`r{u4V!h$v zQRV_tdl>VQMY(~k9Kn@zeFTds17Bf(IE@49LbV7sAgcqjfDrK=mSSJP=R_zt)`Gpo za93?WUr9wH%kDe#jbL1)9IYka^L2mdHI?z*M3kqGilE*G52#a|6-Q?y$avZG(90}( zBCiML(Pg}3x6b|zF#Z1axb}Lt;#NCyPsQGtY46G($>A7?%^8sZ(D-=Q!xKV=i^=(wovAWX3hc5~F`m)if2(g&|p5{7YK+ z&%~x=&{j;*BKytB%Vfl-pZahwQV(O3=@{CrRlkzZtAsp5W4r^kbfd`}?T)j@G&TVZ{}l^~}W(?qOJn=(Q3iB}o`e$i20za=M!|v)ui~ z%WqO_PGlYcB-?RrF?7N(symj@=Gbs97ks1?=p8K$q?~%fnPyH82p3X^VyzUN)qVWnr zqfnQQKjHI^YA^z-R%Oy!;mta5Hazs%bFOg|iUxm9?o1~UDJ|6Xr zzL8N=LnjE_MIJnJGnoQea0i#qXO8=^)DvCBE9r+OJg|!9OKN9o+goL%NXpG3QEMQu z5ygCMpaP<#ZF+PN{0A$Ns}WqFd$CnGA?W!amReCgGfLA%>ShIJLD-pU@ez zM{ZpiA_E_*#~Dr3#6eKdw&5+FW)jQ<_>T1Ef(oqo=o6MRe+p*ZSTHvX zWfO6cm?_WuOTf&*T(*d;GCT^o9xMb{@9(6fV8vWMuVuwDm*c4)ii%G)ufL1E@~=FB z9Xz&gjvz$-XOaS2#l&0XUudw**n-}Ak9Rg1f=5C)c5DZ>dI^xa@Q(ZV;Hb?zng=cz zy8hxzjm)B6mdH9tnK2H%I3W(b0oZYLEj!Pq)3LQ+qIMFIwIU=h4vv}i+)D8;@ymO= z#%ec%RZbaSj~ubWcN8_cCgAZkefJ5%kw?7?!&S7uSdMn-jcJ&|if#qlIu6Ylgmpqw zUGzMk7b{WB`NN5&kffN=_Nd>x6Bc$VXPMq6*ezipnn6i>z=C!RcQa$S?A8wS`3ddQ z6o34GQ{c*Gx{u@0+isM_Jy1 z;<50Lx3aSr<=4h_hTCZS`}JcYPYBE957Njl1&l}-J~wOOpn@loAx#n#ApJ&_p0YBD zLe}($sPE0)_gUe0Zm!?IcLT9)h-AP+uz5d?z*~yNj}@Zsyg|E`<6VC$ydNh)r5=z4 z+sPld7u4gkW-}rO)t}&DP3Nf|`k)bANN<7}KWs>GSoSD~K7RA4d)&nz+_CgH zU~a2%fwbR4(-(4b$$$a_0Vc}$zZv;%`b+&cHUBmKj0~l3OaB~k3x5s;3;O;~FZxB_ z*DJE4`TT3(O+h!Y;Tj~~;@D)77{C78t%G2a=L(VXzs~wgV+?)xEe;Qi9nQYB1$r34 zLJ)Y|1)xXZ(6fd(1NE2&pGq*wf70nCuKeqiHj`g@Ts8IWU`Kb?rM;8Ah%Cp4?39hb z^7APg-}uSWy@HR-eU$J3xfoW1S+97KlfqjxJiifu!0D~ zv>AQ81;mAm%Ys-B@{#I^=oG)?X)|Ru*^6ee{D;@nQA`cRAsm>DzU` zV{!7b3!TB^4e_VPhkZnVg&;r_^0~$V^*s)eoeD{EUwB(0;b)>}&6o*A?24ijZ(Yqa z32>%H_)k1;l+3=N)V$LN%iaR(J~Qi!`Pk&-@K(2vaibU2EB1f$6cDmKq**q(WcvtWknVA7Se(&XMrgv{}Oq6s#ry#*Z5%-p#sGD`dmKgU@%Y{F>t8^;F zV!l0?5D}l=)Ulzf+{-M+PDAG}16Qm1!5t8XsfiH=0oMrq_t{-DAAw&WxNZ&kkikiaZ^V=e<54ZxeWEixD^8K=s@U9KDeT@NsoeUd6Ofj@(t8|&Wymb9+ z@!nW9wpYL%fXqut?^{eA2BBhZ~DbvNY8|^mlzRVf2xNRWF z6I?s}63&c8l%f>lIM#48+Fe|Qt9R;OK53uTcD^X&ANJkg*{a*X2 zziAIyC~F4?GUXEOK{1%{V|mTnWIW`v1BQlp`F9bNe~JbQrj^^glOYh_2zU$e;!Ho` z1tNV&?`P*q!pznGP83Njpr<9!tpI(XKTV01=t+9~Tgo9r^J8?hVm1Fslis&T3ZTmW z`yJpsk=>0#zg!JiYsSAhc7SKSI~B^vEEEY;1;vai42mx-G{n3d#|%LSD1Yda?yx7f z;2yxJWYnEM6W>k}Tg_CG`lXfc9gNa;r$8Z=Df|fxPva#b9G=dXuvTX(nf}7j+~UTH z$b~Bp12Ig?X?=-amm+BsaO4f#(Xg9UO%8c{kArE#DiRQaNF1(sIaR{$aPI~4j<7IF z2oNh&DKHv4_B5L#{(-{-WjKwSP;0K#PYjM$fBDGGYN;Wy3#<6ccT`we$}BDQrJ#~oY$A$z;N~4(}u_E2bSpZN9ELdlC%CC zE127kI~ATBhz?_~w^mh3!94yY<;*0OGQ7fSgS54k6a8oykick0kufH0G+|+AvbR$4 z;^jaKHnw)`xnNqY!_73X!rEA8v`Vz%Mj9vtQZqq_^S;P-L6AvutGEA7oJs=k z-wj|07l-=_-nZVG*p%YnX~6Hr#R=`>s?WmZD_Bi#&may9yuY;HD~dUx#J6Vf`o2oS z>`*EtEwxw-VUHO_?_qQ&_$O9qXx{XW@wpg6wi8Blup3Ti0gQ)m0RM(G(*Dk^n3jGj zR6Q@a^IX=1U#C&$C9lAoj@)4&xa6VY=-O7iQU^b)i!RrKH9uPbg$k2pMEaXh;LK$V ztJ9(PgpUbK65sQ%T*%w6(Lg31;N5&iuC#{5mV4|aKlGaD$4sjgu0b9P`n&9=@>`Xx z6-%yaN$SrX5qHQZs>`5o`KNb0NtNm%luoRVP|Crrk#`%U-J2>@&!b1}myc=TTfp66 z#?3}CPRDAnatIh^rTJcSV)E^J*5Zrxmd8p)4y0wXqp5BG!?lKSz6wx1i&$=C?<15Jv8xIYzIJG zd+FdJ+^JCG^4&^iKa(9|jl)#*v^&*Bm6d91W6id7}8+1v*&t;6Cq zIi*bH8FZwUMMD=MD#wDlda?G#PuW?``k@GzM-%^G9eR=xwZRgeLy)o-B9axk)~SW| zn2ws_(dduQ@E5XoxOQRXM%WEXwEtS(U#bJe$fQ!v1 z-){D+pBQAunaTA6s$j`~E2mG%|B*DR!&1}+Ig^C)9%=qqMnKoI&hcn;al?~~j*&~u z%cEtaZnBQAtnC1cvdfZ7W}rE6KETExWBkZy`fzPv`D$0mNVb2hfoG!Vxeb5}W)~Z% zY{WKNJ&4k}>Vk5JyMQ`Q()N5_HhDM=0RQ&!^*oHjW;y9UM;lQk6uW-OoR1 zUiZj-o5lmB!-ZoM>-i1>zDT8*Mgued?$-pU8Vndak^1*p;NT?(D@DW&LFda4zp(D# z3Uqpc%WQ8D?9E+HcgIon@Dd9VL@BjE_eGq#S`9K=_ ztzZF!ebJFQ{c*LG%5P`y=L;?u@deK@_Hwz+3;OHEaFW=;&MrXf1m{tHyyNZ?cc{D! zgR@{`4%*WX_DtB8Fs*~Aoxij01d+scpF*~6;YSEPCfy#W7083?}K#5p`4I0e#B z!uJ?^ccLU$ao(Sdn&5k0Y+8H?TOGFJ%gbp_z4NTF;seActkv0Herb?%Yv=VZ3F5pD zU`P7e(csnOg_#v{?=2E7ll2{etlGo;{z=j~6E?#_DEPa?`^z1n9SkoFUb3B+hO+Q* zhD#p?Pa0BG>QA{;T8LpdbNJ;fQyW`ws48(RUt!P;4I#~saWUB3qVZDc5cA#N$r=3B zhcs^GC`HRjm^P9@t3f1$n-&=LvG!(p#hnv>MSc)QkBc7FbNl6}!DYh#DMCwu3@u=S zEtD$_T~iOeaOT$j_B;zFNB!g8|1WpUGJ8NPSRff3uNq6I6&{ia6EDj+#LIFIk9X}upG&>o7)@#0^Y2sU)rmq0%>9U+7B#AkGgJpqg$PX`vJNk)dBXR zs@6yihgLhS09gf)wWlsjL~t^v%T2~aabzf4b6fj)f@P`^a)D2{rrsLf13j$@ju{sp z6mi_Y$UA?S@saEdqCC>m*pL2U(%`J5;R4=0?BEwa3Se7NrVQig6?$ z&re^;;1|injpP~dhDM5129*OPmd4bQtr1`rZOW5G#TYcnHwx~%_?{#ne=5U}DWMvK zr{~!7f{~F??jA^HilAUlt&OrkRBW&>XFz(iBamxbmI$fS~%LIv$6Mi%{xM%fxi?~SSrA`1V} zL5j~AEvcRh!(oXM;_X(f%={M#Z{AtH+j3~BHs4 zxMTi+^S}}Q_(Q@Ib$UQr?xoi!R5SpD1S1oIR0)-Fanz#_T~Elb+o1FxO*$3~)6J z-u=Eez5HGz?s-{%)7CNqX3LgIL5-3V7+R}0kFqJ{7kKPokCYiEIo+u+fQYSWQb|d+ zA#qs}T?ybFRZTB)8FtVCC@&d@CD%{Vb+J8r97itsu@&J7$M^MVyHm6GOIZwrNxRZ1 z{n57zL_!`660`B+%o`5?<8WbmbPuna4kU|Y$V7_LdV@Gt?_)EF4$}Y!wKAiifh!m;pkHwSR|dH@6GCFV3gN1T;LMxzaKu)lxXGrAXpfwZ*|z?R_JjMA{kr z_6&cNKCx(_V@NzyFuUwDAGF`748EM1j{I7&y-F#n3lV3vi~%FH-_Siad%Z~j)Y6rr z7+>)0nH0KtHy@;f|2w1qSc@2u3h}dz=?d^eVB*b~)OxJ{&|kEeTBO9m3ybdxlT2YD(h>Z9j>j=DQ%U%#k=fc2 zpt^}jNku6}vCxi4E)-?L49dbZDpUE)a*=w|X~jgf!^$dRh-AvdgUVmDlEkQG8xlyT zPb4ghc_2~`3I>#+`{Th<%Fju1bbm9J{%Aa%;rRiH?Getxk7`OkctDIL;sdzH*;oVd z7*in_)hsgTHcDAbiG*DQdxA41oQ{yGkrG6Ez3T#lSdI%_N(uQTNk?;*ZamER3$?== z?8+@WF*!5B8O^tu6rx^*^s{m^huXLM`CDk=Xfj*wn&xke=dmvP%YN=RNLM}t>3w{z zd22>TCqH*AajLEbUEZ8RgImRg9~=xe8!C~WUGk+nvBK_!WsZ9Yqh?NCQGiGl?+v3u zcYQ>cBw$0*v*JOCrq6$)I|H8ayc9=LQPW(vI1bq^wf{O%-0@#pANm6KjRdwt*CIEr zifPowU1)Nv%YIHNU3=y4VNdYn<%OuG;~B@Dk-z0ks6pQ$l@yEo5id4>{42OJS{jbB zohAAWC+=8S>pC2(!NSbQPEHk)_OrbF^vncVa9F^p-kbXAiA#Al#4Slr+7+w=CC7f= z=r{nY>E}^c&iY^S{uu`F-RnV1N`|G*aXdaKIjcPO;v=@MTGe*6BF)OR2|wfDPftlJ zmMK}#RUtvip??5kq;wc1;})n?mRL+2e`Bzc>lfc3Conv^fVcF5?cb3%g96W~Gbr{9kg2ZF9P86gE zp0pnX8q%j@x`^v}h!g|9jK0Raj46s8L|2bWMZi`wM;9~s;HPEJdjl!2!R6fA(iEup zkhCvv?3N%Y~&t`5}S!<#8S1x?k76FvVW(_GeHloU4{-K zuZENuTRskBG=fKqb@F&%ja@>y@L@k+zYS>B++C;jBn-73TV4sXH^U#ZTXxGN$8YwF zw(BmQn@6XQWk9~6pGna)Sx=Y^{dCdQ5A1THFT|`XL$TC%Bs#9&AjywMI{Y{-6_>_e zf592x(*{h&16}6Rm34$eeI(Hc!ti%8d_Y}U2ll0~e#p_Vf92QEtP<+9*Bo+QfCCPRj^ zK$m0xxXSb8SKQVytb*ak1c&eG_~F~;w}!h(1+>APWPloo8vSV7z@$inL+k!2+?U9K z{v@Md^l48)K_=&#JWPhf1QA4RUbTxox@kO!MgjL=Izw`0a{pIdLw8nJbf?3sv7} zDjH|0)ubiPJk8axA1S>SvdGDRAa8|sEjenfq6O)@3o)a!Dw33uu~ zpX~KfzOqwFO$6n%i#@3KUv`zZ6wu!)L_RK&MTK^&^v0_<^f}uG(yyNUbnX}7YD!=o zJ{-2mI%-+`zXQlKoXbK&}|Y zjq{hn338vYKaBYd#Fj7jP>gzG9L|CUwYpp}P0pqFr`Br#)!c5PWtHo{-v8rM_u{&D z4&vXb$D24WgW~uU+zB+|;$-u{jCb2u3}gV|w*t=9E->fkKgod#G}8=}Z4c7J|iU$PcvtoxBsckOaGL=2@k%m)03MK;3ud3~7?7dK@W>kJ}Z-GYd;1_s$ zQ!nJci{pw9kFO<)pkkgSk*E}wjRo8M8)Vtc1ivf}2^?^v(2{EF2_RUqNhco7qxGU(9fn02w-u*!cEoZZ8`D= z^zt875-*yGb^Bl!WhjpBEihS45h_7^U!;S!izzMmvfR63Q!4q0Gcit{kl03yjOB@- z@zdtB3;R*~d_>7MsH<}+aa`*lwqKkR34|eMLC^(nhkoiQNm^+HltJ5Y&$ZkAp4Xt| zP?yw$D@bzgc!X2b7!;N*Z2younB3t_kvgO_8Yo_McoiIvP3K;VDZwtW9CJjCu7EFpN3b|*^VqSQd2lV6p$ zKS(hMi9)U2G*b>W(D-0wk+n0~7TJt^xb*k^9MjA}eUSUjrh)_o!(2Hr8YULiqjnsO zyE}8?xB>kCNY@3xk94_37F{6JvxS3QjtN%MJ|TQ+5oGU$$~(HDS2bG)R(8y2bX6#e z=A9viRQ+=-om=Of?gDN8h;ur!s>|&g>AWJ^1|0UyzSC@Kx^C&8feQPHcu7U^>Usks z4TsY$IAn|0W&}Mp>6)j$dd$I>?AC%){Gc6=s~=|mVXh>5&!E3>q|AgtkPazVw1e?M zL_iMA!z6{}ha(J%D|H6~e2_%0Yq{noM;UQ+P9D80+-Sbumsvl~C2`iek@*l_CD-J3 zg35dZJZ}m(`h!Jwg5+r2)dvz`MlYm;+F4nuUr?D8)JD$?z|*Tykg&k9r@!QT-z#PMI+S`C zov*K8+lfDgii-`UYJ=Z^YViMIm%2K?leLsIz!l=RZ9VErfn*Q80h+rTPboK%ia4Cq zX`CqthJ{(m`Q}b{AX83Yy2^XHY+hRD8o8;zn}em4hN`V~CDL$plHtsl!^()VGg0~= zB|;Ry{aO#>TrsSSV?U5K+qf4WQTY>|k7yWo1Lps=byi_*b-#X0*jH#=Ebdp~o%V~janTa)-FY3RFS88KcG zGFsMbLWQe{-EFZ{xbuy5prf%4S;94VKRhW(&bbPd&tjWSdr0C>t5+AOWCr%9S{1M; zDU^rQsI#ut`rA)@Z9}Ph@)w~|TBe%lYYoa^Rwc|#0zuajDL~UNG5!)5)YQ-N zQkP}`6O9dB7gZEF(9KK<I25#3~j}Aw6R#&)lg+js6IWjT}t|lEpRH@}nvRy<0YX zJ2Blb@G$$767-XT3&P?q$7GR$-=Ns`7hNf$`3=c?Gf8*^JS}%<4crGQwFhzk3J2l= zztCHb)~f)QLe5gP8tU_84oSuTTpEt%j6nK_QlD5@SQXnCP**H5>ab|7d+vH1`HoJf zh|YStTRuyXObY?c!U|H8#mgGao(IVrw?~cH&Ywqg8Z3}JtA1^_kqR0SNBbbbG@2~K zP8ABmDLZ5b;cF2T&tFLHR@NMluY`}pBs_R;qST;2BR3%-d~<-FFs#bK!^NefrIl9v z7Pu-gORBG@!S?xleW)1YOEM4I#>W8`kclFHqhvetxe zg;Tji4<&Az0ik<}$CxxUhPoK(~dD2Mq!8`7QXm=V$C#A-hC>u*m^N&W)arMh4mzcUO^CU z!a(}6Nkvl)^K736SKY#?sa-+h^C8otW`t0?c}5Ig$6UEv#$Bn~-SGIE>sSnWe*itg z%%=aFjXM%he3emvm3OiL-ixmaww|wTkzWm%p1Pm&BfU8)_sP)RdcsfOYqqSd=jqQ(!vh%|De8C)n~*LhPO{p#)qspb8hR z5C)@}(Vz=CKA=CkvdA-s?_D60`5m4_bt&;&?)XdT$Lsqlts61S-<|Lk&=2sq58Kf< ztI$eczNANl;20=fU{ghQ*T6QNk0a+TBp5L1;B}+Q=8hw_r_>0ZYO3%ug!;Q0u6#rf zXGun0=}DCrD=p>-Km;|tLT03+c1|k~-1&T7N!imdRSM_#;T3)+W2aa|zj}U_6l_DQ zG44atM#9~;Xc@03h8>CVUir=w>7XF16LYNMB951kIvnh0@EMK^_rRi206d1(`VI?v z_}QTB2!H*g!KcBoz{6V9fl(fCLrT$CM)-?-q@U+h9;@^{x#fk3S=XZoWd-)pi+src z8n&X1W`TYDcOC(cbW2qn@N`4^r3f*nkGyzOo-d3_JS@P~+;o=DNPQzl= zr+_>yEHt!_1E%9oYROwUnPUPprqscAVf%dDRFWl-!Qd!$~){2b`|D;ch+P2ZN+{?I5T2Z z?cB$Jk>JWCY1)*5AyDFO`7u~9K_gqB%>U~^`b;F%LAOVi38#Am$2{Er1K+IR+G>wU zpO26`5pp?tu&p_M7AH&(@=v4%3Dq2g)ma?iiHExr5OBKWB}ST$e&4Jkkg1b}A0Ttf zR|i=&R_GUj*6V}~zoMkc`PMI=ZC+O{?bC?jtv9-}n+tNaqfP}=T|lS23deMAD4c~^ zP#{_I-E19WR4#N*$E+YNyDiD>hQs;35f%HR{p_f~_d1kcHfvB%uS{%@hoSB7w}+;$ z4c`<4LiOmUH&g=M#!U=6AW?b3+mZWRTo{48_fB+Dd2BS zngZH9QhYTj3;lm~mSHM;vWmU!*X{k`i}_CZ(#Xy{>Pq-l$9CwwjU8k5$%d8m4(@Mz zV<*nTS=>eZZC^xYW~daI{8`u6Cqb$A=Bj(g%!h8@{M}UdgL45D(C%17$V{aH53ii$ zk@`wC;R_5DL1%P=hpG2`{-Vxg4HwVb$5kcGdL1M2S9(LHe+U!==+1Vg#t-bk^mhg@ z+@y)M+JKScNwNfO*&RcF&k2DAxv?hVmiq5bE1rMIsWi+{!D4>JW!GXL1kuFSZg znwLf2TmyuJb8(Sr(-yjLCa^nwQa#IxbPXhJUfm5E=HWQ?8I8TGxjwONg&@I@$-IOl zAHAKJ2wlzDW%XXtgSsl3PKpl4SvS5ib~a7~zGFGvoW*8TV>LE?#ZHjg4&SgrDgsq%18{H2QgjsxXd@|dY2h8#C>wabA1!0>_!=x z-s!+Nysu*Jj?2$b#x?qxcRUUdjE2mXFr7?W>|ZX5zfQTDlkoXSnn_A}Z%^g&+l5~E zWb|;J6Aa3jKL7Aly~Z+!sn`Bspa0#>eQJwY6a8HQOi89fL-eaMz&GRK$f$ zGf3ZjZnd39Vaa*-3TeOJb$r-43->OM2T3|Z_ikoqXGbL_-al!en?5e1;4fd@V%w9# zC8dx4*&BRP457s^sDWfCGudH@oX8W1j@m*_5(^nN6aSxgWnfE#Oj%nR zJa+L_f@o`{zxOgno~@<%gh{-;jp3d|%hgHy{H=Qflrz|)`ADpOMr$7xWWKqvhkc$jTICZ{E3doJ}ijSO8HK~PC(0y&hlkj|f?r&sh=$Z(kM%_8`1*Dx>Q~+(!pPRKB-Y+;v z9AO?7QQAbXV#YdERuf=4?TW4EF`XIZf^vsQp+!qyDx74Hq(r z*q7)`+s=haZLoxb?;gffsr@6ES&b|tad0d@>F4o&wu{CdO`xj~D?7UMn53BNxz*-R zN&c($&KD%i(R1QbU84Jy53>(FI2n=XdxX{xq@0OACSAH%@Ru3_p$9}Ey&P*vcB(jp zG8%?>bHgF%UDq2Ln(Tf>L4q-UO%XhHZdTUlWs^V3ZpF@{PO!Byxu7%CJOdsdoOsTU z3MB!2q8?E_x!i5^;}@~1F~<4|VcnBSM7UdVQ@bmfzGLd@Z`$@_f(_`$%Z|fUqC$=% ze^JK-=r(vL14dOMf z=Web56%7rE%MJ`eOtk7H_|)+*c+|hUa(WRs32Sq?!`vTDMV5#}60(Iq_P<_^9vN;O zOJ^;|$+kJcu-kasO%ZbtSHh#FwW{GPcMuiEAE8fwzx4KV=h#C{Td}c?h+Z@vKz)LF z*n^OMlhxOU-9PheBKPn{8juM=p8^wmNXh!U{UjQ!Z(R45=fzD~C5)z6vR>y?D{?zg zO7UNjo>k#^(r_s+aw6GW=DtK##Owf*hd`!~4lLFX17b)>NO!NMZ1;67q3wQ+48-Cb zK~|`DI|DLn-JUCUhlT&EO2>gE9t_VIG=5bkp;J9CXJ)`FfDDoIRaJEMAS&Uy^TWaV z4OKOE>@kX#{j1rBCUW9%r#_O{`a@v2YQ}9=Z!w}^t3&A355g=?b4JhJ4Z`ngXD7uH zMW&)6z+3DwzrINPdStjdZ!B^lAGovgMcf^?Wy+fk`7F^;@ROAQd>Jq{+RuhJ9y;<% zvbV)Vmk!4@_YwMQ5II_bDR5E`af`&WixdH!Ov=Tj5v1*0>HHlz8M4LrUq1G}oY=Jx zVXeqvZLy72G>E#>kUpb-r{t}NQuUz1Aj1o3m@}#TX{5=e#N>K{W-TG1%5l#k^my{w z8@vi)nVdaZI>}hkjDmOE0;`eb2<|UkXTCx`i3($>!iWwV#)^o8qTa>=!@o|n zfu9MF=R0MoX%h8fXJk1JwdP3gY0hw*$u|}VS7;`58zcPYwPFpRO69* z-o6O&!rov8VbP(q-1f(%sK@u?i2TC3;n4_9Z`T)NZ-*&oniaMA*T=>qA$RD4#KRst zUVqZml$3AB6QFKLk*rdwv03|`VC`8)_*S`^9mr%HyRg7vt*JB*QIzr!UT(5#l&n31 zRjQZBM18;e;~GR{zSpj<)`PpgZiySrx6*1%{J{Uamg-pw?CJc9u?1a2bWLkTOCGuC zM|Bs&@p7J8Ki5TBZ@GI|jD7jVk0R$NKVELW%I{jX^=Ihsq6^3? zeaO3b_3&sgOs5jW%iWvvkZ?g;P4kMO8OTo+GTq?EnfL!!~2cJ&X8dqtGW z0eIEv8Tos34wQ7mIusAGN2WW=dO)=mdeixREt-QC@8SVl5YPSY0p)`-NzD5cx)3=X7aw9g(NFIH zR@QZ!T=oDv*0Y{_5vljrDjl)^?8j8SakJ;YAk~A4a69ugQlrzO#>#mKmN6uZ08t@} ztH6^DqJq76Ujft@h;O8Y+fP8{@Yhc{%JC4l^z*9*zUq3J{|QQ1cmyQ7^FLeRaG&qG1O;|-5AlB312@Z{5p=p4cy<2y_^k^ z6wr#3Ij2r;H{Xy^R9_mBd@UFOfMCrBn1tE8zF~&!d9Elr96r~z&v*IJaZ23*XrkkfWfI;#tQbY^} zHAsocE}N=06IKXs3hy0lCBOeSq69`t+iQiqsu@RmczTjV~lm=WyPIU6X& zfZJ`r_7Iji|F;-cVsh6D8i`uRLPdJ`;0yT-Wx04>+9T0smnNN*W$MUTmfBM!ytLB zWH8R{>?G50@{Qto4g$Zf6l;}3VSF5tlj*@FjCaSo-I1Rh+U>XVq|a~2UD&E^AAP0N zi^Z*@h$f(fr)u8|@O21D0HJB~?j-z6VP&+0NNpjuj2e^d*`|%ZO~Ai1#he)=2K5zj zm0>i)h(~@=HbN(m3CEBMdT#dz!;FnhrRlMA{JEbimR#v@!%raLht93N6TvYZfFB-_ zNq*>5RYJe7giq*yC(!8kZ}KF68IQ)|Oz-*NuG(%zqg#a2{|OX@X2uHNloI?BDu!gE zs@pnx>H6Uj;-e!DpC5OwK{>j~$L=Hdq~12}q}7D$i*_p7gre5Xs$2Zspft0;JNiR4 zQt_4(ycv!ErZ~*|dDML7momHEflyquVRUk?b*@ryzWkp8XEXqEti5wq{M_lpGjxvEAmr9RNQAPndoxo+<$#8t)B4VK$V{F!|g+u|x zd2~BoCW3Yj*nzbDee3Q&-yRcjq=jAfQSi$2nbI@bKpn&M>D>Mqt~}NZFRvAz{MCZhUAaeq#9SG)`1dlkIf^zppZ805{E)Lw z3Sy*Tgb0(t7_;Z+{Gk~H$Zxim!)PYrz9Z)Y-+qu7t2g?!mBBE3pX+(UmoxhQiq_6C z+N&3oBoZ*U&^6Oc{z5Pqs2>wLVU6OD6&hLKkJk1@X*6o|OnRtXWPtD3ul>F<^Fr!8 zn4o(fZHj!5w3*2KA=8k3XW($|>xmO$)f!$Sawnz)kwn0YSKPBrVW2TTBn1~FsNVSS zD-k(-$!y^5^_ljA7Nc-}3V~zF!7^QkSN^L{*W#bF!Q2Yy00BoGWWbo7NJJ^&XmWvg z->SvOJWkfMv~-Fsebru-3;v~2H(srkZ`U{v8iN8yL>fSJF_4tZv4;e1 z)b{6Du)H8F923U0g65K)%=0+(Ud89-DzGB-R5U&9)@^9 zra~vd$o+sc4tZ};y6SxC61Bh8@sRx8HB7fTh|KrG!8&&ZIv755bZck0;1xktMI{VP zySdmLUj8lQcj%F`CZ|TrP`buuntn0w5hj_Ln6IzE)zy`fPJ4Iaq@$#mB4=~d0@dz? zDGniN%E*}9+MlP0urLY{5u>Z;T?P%9S?QJN#k=O88#ohRnw#^7E;#q8r0YKa%w>jh z7}|Jv6tAL53UjGHQQ!Icw< zP0jhb#=-}gP;?feg^s8)lP7mkiQo#3Nh79qOyz*wD7pbKF7>JrBKOH#oO+$?7DpT0 zuy8Pv7an#o-QjrWA1jJ12kr*6>^o+`x5)MRQ}%CJnJ)?lj{Vru$tyfHq+~Y5@XX(8 zn7c_b?inVFQSvqyv9+*5(+`toJID!0hLr@fm~t+q1N`HA z$1X0fW?@_iczP(Wk(1r?h09;y4^y147pg8fDW;-T4DDCAg*qUTq&}WW*DcZ1t1ne= zAuse>)Q>j<4_QqW+?zVWx6e8C**QEQ2V`_lRPZ=85D1wrQreekb|lb8C% zxC)u1-#MI!?5%KCT`xd4q}H2<>daZ9WXc;RL&+qCIfV&v$f|E7^4ozrLyQj#a?_nD zARAalQg5q9RNi&WkPKC&v?Nx6BVQXyX!edu?0Y3vslf|>8s1;vx-N~ZOHGE9zsJd! zDo~pa=>8X97|vD?#Qv~yM$jzw%#?aQY{TlvzcL{?{aBv&2LH7dQameWDphIrC;yoV z3+~dcANB(w_%hqc{01GpmWeug*5P<~RAT-!({UpJDwP#GwCaByVG`Et-hZ(eh@cku zF7kJ&HWHFlE-`Y)frj}-=aDAVc#bsY>Iq-6_N2@-o|NB;G_>n9@T$tOVyI!LVmn7! z+v%6^?=0!bMH5JX}7>3kbI;zNIR=>Ct zxrF-=_fo>Ot}PA^llz-a{CEMxWfX|Dfb1fVuAPC~HL&|~=VP}&lUEcCQ+{q2DJ1$ao^fZ{*X&9ix1Rk| zH}0fFsPbY$rw7f}kDbRs7)tWJ?Q$1{?ObLK^O1G(;>43@rfm-=k8`1RxNVq_^@Dzj z5LmNFJmSV@Buf^zFL$T|B#bUBpU#>|iatdz)rDzE(7GDd5;-l_4&NtKJCJZ|j3cG(zdssX8#d1X;K$sM*l|qhAm1V0bRl)(_E6_EO;3>iL~=Aso0$XXgOMl1)*n z0QueEJ5^iZ=akGFA)mH9KprY2b&nMZKw?4q8a@tPs|DyW`qXo{?humJsb- zxCbTLwtGhrws|dOe<7jD>Q8EQKieA%K{CC1<^Z99&oIiv<^6cwvCI|fqa+v)2yYYB z#b}5Jy{%tWSn*b&u??xu11@*4s67|z<26>*Y=@ctC@_-}1P7dlgZDqW+RuQt#lpD7La_hp=?4pqa%~um<-6PxZ{%q7}qiU%i_Zl0l59Y?~}YTm0&~-rn6+;VTG$4ao@=RC1~9=^msC;J|^5 zD~PU|h0Ykq)u7NxE=>23@D5nh^*)AlUak($WT0n_jJOil>E(t=%)~TJGKIRAIr!{c z<=Uw7*9NAV3qTGT>*kd|q9r>C?@AIO$_Zxs(99go$y;vT(mjG{+BkXx`C9V9-C&|YK``$@1K z(JB^IRaJF?w`)CZ3XcoG9GPtUr@8<5dqyed*S81*4AuOqKEeus|a#Z9ZV>DCX5K}a>ri$(b)3XZ!T6IZ)Ju8YW-KfJC zi-smmAlW};&Pt7g%r61MJvPB2VdE2S@qzB+$0EzgjHsxnBmuL=hoCrpMDtQI6z##tjKmQ;M!vgf+cA*GXGQmRHn^GWL89QzSFcbt2(l)WL_$A zAy^F%T`QBLJ5{tO2cu%Z*lyscWA8~2kC&#MRirwJS)mTt>51CTuqY@pS$4$#=-_3J z-vIyKw|2wci$k_Rk`rjEF|MGTVKQ6wnm?qh^({iCgnH9*Oy;gj!HfFgXXcw;l z-xA|zf4vtK5{sH4P+(mg0_%VseGZ0UwIA~|59HrST=(0o4u1`)uHw$pgu-*H40O8?|1!~LpyO@mMuR}mD}osuAX-BetP=8;NfBQ-DJX(vvVr| zcTnloUsc8OKO{m8;c974+$wXgAPr6?SZ>(+GVogv^Ki=23vZAMj3<6TWtvWu7n;>?PpV%H$VEuNG5V-N?#34^?uuUIV8(=lhik5_!04TpA80n3OWjacfV;T$07c%Rw{ur% zOcBlNJv)5bT>8s8Q-H7vA{qYhU04agBeh zh6q<8$Wz*cE2`&CUz=rYP1ElolgLl@Uy>9IJTULQM3l4iG#o7kd%wT#JeZ6LdLM7v z$K@2V1VH-bWEUm=4Ov^u*j=1wGWq6x4VoS<{hZ(a)X7tDe?Q7&I=6x=N5FwoDnb;K zwWNMAe-%g=X850C1Av$+d3}@_UEQwA3hLHZ8F}lIm<@Rs63M7V`SZ{amK|39q6P?z z@{$e}X_Lbzj-Qt57K(ch`XgHUk|_!o_}{b#taN#Ef5dj`x%nuvQZszRe_kv*DfZiH z^p1NiY0eMvsd;Z;^Vqz=jHgkLB~N2)TqUh zyYur{P=0NJGr{yv6Ze95wN@@FZ*rhnF5Uu<_^FsbBv@z=^^dR)gzuL;{gNxwD6&bF zvEC>l)N6^6Ew>Z!Zi9*^8ZL4xXPT$#MZ(hKjWnvicEtRra&0e2qI_a#r$C%+v%<)z z7jwSc!Jo`Tr^3*HqeO=M;oCdpBbiG%BTJkFKeO{Oj6dvl;qO_@7x*SVv_|_$L)J?u zuW>cIr=350`XJ=RKP@wJ$hM`xs^g-6U!z2!Ck-x>SB;Zn1F(K$K$xN567h8gNy=L) z?AOWE^=K0XV5V?ldd9cM$h9OuoT7q~(Z)hv9%*oJaOU@kzt-h`WDeYlHP@ik5BcLk zM@$X;-i|byDdzSM0{J&eDY87+S{LELvN7hDEUc_DaKhCuEkXHtW5&TnR@?lIy3V&2 z&S4Q@V%1AphW>=DCM%ai(|sh0LUSR1!Cg)d2j<5sZOmt70GPv1+9^IkgyudS%~QCR z5Vj4Y((#>YS3(j@OoBs0NX(f2H*SAJcjTUV6MDdhI_!RMV3t8UN!K}mMFY}@<6!$L zUjKdt`e5q^A@Nt-e{fvzLU<6Yiic=R21H}xAM%J^F!jHG Y@C#*dzd_1phJsx3(n?Zw5+))44`&qi{r~^~ literal 0 HcmV?d00001 From a0efdf92a8c89d95f74ae4318fd3ae66089f5f3a Mon Sep 17 00:00:00 2001 From: mayue Date: Thu, 18 Nov 2021 09:58:40 +0800 Subject: [PATCH 21/42] * Fix bug #16333. --- module/testcase/model.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/testcase/model.php b/module/testcase/model.php index b02a3c74b3..d92fa13aba 100644 --- a/module/testcase/model.php +++ b/module/testcase/model.php @@ -1522,7 +1522,7 @@ class testcaseModel extends model break; case 'title': $showBranch = isset($this->config->testcase->browse->showBranch) ? $this->config->testcase->browse->showBranch : 1; - if($case->branch and $showBranch) echo "{$branches[$case->branch]} "; + if(isset($branches[$case->branch]) and $showBranch) echo "{$branches[$case->branch]} "; if($modulePairs and $case->module and isset($modulePairs[$case->module])) echo "{$modulePairs[$case->module]} "; echo $canView ? ($fromCaseID ? html::a($caseLink, $case->title, null, "style='color: $case->color' data-app='{$this->app->tab}'") . html::a(helper::createLink('testcase', 'view', "caseID=$fromCaseID"), "[#$fromCaseID]", '', "data-app='{$this->app->tab}'") : html::a($caseLink, $case->title, null, "style='color: $case->color' data-app='{$this->app->tab}'")) : "$case->title"; break; From c9baf6fdddd3d29c7ef5e67f67c12ba794b802c5 Mon Sep 17 00:00:00 2001 From: mayue Date: Thu, 18 Nov 2021 10:04:16 +0800 Subject: [PATCH 22/42] * Fix bug #16332. --- module/bug/model.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/bug/model.php b/module/bug/model.php index 997097fafb..8207008e50 100644 --- a/module/bug/model.php +++ b/module/bug/model.php @@ -2752,7 +2752,7 @@ class bugModel extends model break; case 'title': $showBranch = isset($this->config->bug->browse->showBranch) ? $this->config->bug->browse->showBranch : 1; - if($bug->branch and isset($branches[$bug->branch]) and $showBranch) echo "{$branches[$bug->branch]} "; + if(isset($branches[$bug->branch]) and $showBranch) echo "{$branches[$bug->branch]} "; if($bug->module and isset($modulePairs[$bug->module])) echo "{$modulePairs[$bug->module]} "; echo $canView ? html::a($bugLink, $bug->title, null, "style='color: $bug->color'") : "{$bug->title}"; break; From 69d7441ce39b2954fbd483d289a62e2a885c50c9 Mon Sep 17 00:00:00 2001 From: tianshujie Date: Thu, 18 Nov 2021 10:12:45 +0800 Subject: [PATCH 23/42] * Linked product issues when modifying replication sprint. --- module/execution/js/create.js | 1 - 1 file changed, 1 deletion(-) diff --git a/module/execution/js/create.js b/module/execution/js/create.js index 1f8537bf40..4420527143 100644 --- a/module/execution/js/create.js +++ b/module/execution/js/create.js @@ -35,7 +35,6 @@ $(function() if(copyExecutionID) productID = $product.val(); $product.val(productID); $product.trigger("chosen:updated"); - loadBranches($product); var adjustMainCol = function() { From 7d15430ab1063958c01a11732a929c232581a076 Mon Sep 17 00:00:00 2001 From: xieqiyu Date: Thu, 18 Nov 2021 10:39:46 +0800 Subject: [PATCH 24/42] * Modify the review code. --- module/action/model.php | 11 ++--------- module/bug/control.php | 9 ++------- module/execution/model.php | 11 ++++------- module/execution/view/create.html.php | 4 +++- module/productplan/model.php | 2 +- 5 files changed, 12 insertions(+), 25 deletions(-) diff --git a/module/action/model.php b/module/action/model.php index ee650d11f8..3ca4377d32 100755 --- a/module/action/model.php +++ b/module/action/model.php @@ -1346,15 +1346,8 @@ class actionModel extends model $this->dao->update($table)->set('deleted')->eq(0)->where('id')->eq($action->objectID)->exec(); $this->loadModel('product'); - /* Revert userView products when undelete project. */ - if($action->objectType == 'project') - { - $products = $this->product->getProducts($project->id, 'all', '', false); - if(!empty($products)) $this->loadModel('user')->updateUserView(array_keys($products), 'product'); - } - - /* Revert userView products when undelete execution. */ - if($action->objectType == 'execution') + /* Revert userView products when undelete project or execution. */ + if($action->objectType == 'project' or $action->objectType == 'execution') { $products = $this->product->getProducts($project->id, 'all', '', false); if(!empty($products)) $this->loadModel('user')->updateUserView(array_keys($products), 'product'); diff --git a/module/bug/control.php b/module/bug/control.php index 779233f9f1..f34ed2a3e3 100644 --- a/module/bug/control.php +++ b/module/bug/control.php @@ -52,14 +52,9 @@ class bug extends control $tab = ($this->app->tab == 'project' or $this->app->tab == 'execution') ? $this->app->tab : 'qa'; if(!isonlybody()) { - if($this->app->tab == 'project') + if($this->app->tab == 'project' or $this->app->tab == 'execution') { - $objectID = $this->session->project; - $products = $this->product->getProducts($objectID, 'all', '', false); - } - elseif($this->app->tab == 'execution') - { - $objectID = $this->session->execution; + $objectID = $this->app->tab == 'project' ? $this->session->project : $this->session->execution; $products = $this->product->getProducts($objectID, 'all', '', false); } else diff --git a/module/execution/model.php b/module/execution/model.php index 63279da07e..3c2f11638b 100644 --- a/module/execution/model.php +++ b/module/execution/model.php @@ -3657,14 +3657,11 @@ class executionModel extends model $productPlans = array(); foreach($products as $productID => $product) { - foreach($product->branches as $branchID) + $planInfo = $this->productplan->getPairs($product->id, implode(',', $product->branches)); + foreach($planInfo as $planID => $plan) { - $planInfo = $this->productplan->getPairs($product->id, $branchID); - foreach($planInfo as $planID => $plan) - { - if(empty($planID)) continue; - $productPlans[$productID][$planID] = $plan; - } + if(empty($planID)) continue; + $productPlans[$productID][$planID] = $plan; } } return $productPlans; diff --git a/module/execution/view/create.html.php b/module/execution/view/create.html.php index a9ce44a861..da1c0d3142 100644 --- a/module/execution/view/create.html.php +++ b/module/execution/view/create.html.php @@ -105,16 +105,18 @@
+ branches as $branchID):?>
type != 'normal' and isset($branchGroups[$product->id]);?>
id, "class='form-control chosen' onchange='loadBranches(this)' data-last='" . $product->id . "'");?> - id], $product->branch, "class='form-control chosen' onchange=\"loadPlans('#products{$i}', this.value)\"");?> + id], $branchID, "class='form-control chosen' onchange=\"loadPlans('#products{$i}', this.value)\"");?>
+
diff --git a/module/productplan/model.php b/module/productplan/model.php index 893a17f060..b7acbb6005 100644 --- a/module/productplan/model.php +++ b/module/productplan/model.php @@ -184,7 +184,7 @@ class productplanModel extends model $plans = $this->dao->select('id,title,parent,begin,end')->from(TABLE_PRODUCTPLAN) ->where('product')->in($product) ->andWhere('deleted')->eq(0) - ->beginIF($branch !== '')->andWhere('branch')->eq($branch)->fi() + ->beginIF($branch !== '')->andWhere('branch')->in("$branch")->fi() ->beginIF($expired == 'unexpired')->andWhere('end')->ge($date)->fi() ->beginIF($skipParent)->andWhere('parent')->ne(-1)->fi() ->orderBy('begin desc') From 4e02c5ba538fa04b481b095ee8edc9a2e4112c4a Mon Sep 17 00:00:00 2001 From: tianshujie Date: Thu, 18 Nov 2021 10:48:31 +0800 Subject: [PATCH 25/42] * Fix product issues during replication sprint. --- module/execution/control.php | 4 ++++ module/execution/view/create.html.php | 14 ++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/module/execution/control.php b/module/execution/control.php index c427af1184..49ff183f3c 100644 --- a/module/execution/control.php +++ b/module/execution/control.php @@ -1398,6 +1398,10 @@ class execution extends control $this->view->qdUsers = $qdUsers; $this->view->rdUsers = $rdUsers; $this->view->users = $this->loadModel('user')->getPairs('nodeleted|noclosed'); + $this->view->PO = isset($copyExecution->PO) ? $copyExecution->PO : ''; + $this->view->QD = isset($copyExecution->QD) ? $copyExecution->QD : ''; + $this->view->PM = isset($copyExecution->PM) ? $copyExecution->PM : ''; + $this->view->RD = isset($copyExecution->RD) ? $copyExecution->RD : ''; $this->view->from = $this->app->tab; $this->display(); } diff --git a/module/execution/view/create.html.php b/module/execution/view/create.html.php index a9ce44a861..065e19c1d0 100644 --- a/module/execution/view/create.html.php +++ b/module/execution/view/create.html.php @@ -105,16 +105,18 @@
+ type != 'normal' and isset($branchGroups[$product->id]);?> + id] as $branchID => $branch):?>
- type != 'normal' and isset($branchGroups[$product->id]);?>
id, "class='form-control chosen' onchange='loadBranches(this)' data-last='" . $product->id . "'");?> - id], $product->branch, "class='form-control chosen' onchange=\"loadPlans('#products{$i}', this.value)\"");?> + id], $branchID, "class='form-control chosen' onchange=\"loadPlans('#products{$i}', this.value)\"");?>
+
@@ -161,13 +163,13 @@
execution->PO;?> - +
execution->QD;?> - +
@@ -175,13 +177,13 @@
execution->PM;?> - +
execution->RD;?> - +
From e8f77ac6329a0002d1d39d3c2b880f35c43bb351 Mon Sep 17 00:00:00 2001 From: tianshujie Date: Thu, 18 Nov 2021 11:10:02 +0800 Subject: [PATCH 26/42] * Modify logic. --- module/execution/control.php | 5 +---- module/execution/view/create.html.php | 10 +++++----- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/module/execution/control.php b/module/execution/control.php index 49ff183f3c..1b3fcd99a8 100644 --- a/module/execution/control.php +++ b/module/execution/control.php @@ -1398,10 +1398,7 @@ class execution extends control $this->view->qdUsers = $qdUsers; $this->view->rdUsers = $rdUsers; $this->view->users = $this->loadModel('user')->getPairs('nodeleted|noclosed'); - $this->view->PO = isset($copyExecution->PO) ? $copyExecution->PO : ''; - $this->view->QD = isset($copyExecution->QD) ? $copyExecution->QD : ''; - $this->view->PM = isset($copyExecution->PM) ? $copyExecution->PM : ''; - $this->view->RD = isset($copyExecution->RD) ? $copyExecution->RD : ''; + $this->view->copyExecution = isset($copyExecution) ? $copyExecution : array(); $this->view->from = $this->app->tab; $this->display(); } diff --git a/module/execution/view/create.html.php b/module/execution/view/create.html.php index 065e19c1d0..95d89b9a96 100644 --- a/module/execution/view/create.html.php +++ b/module/execution/view/create.html.php @@ -105,7 +105,7 @@
- type != 'normal' and isset($branchGroups[$product->id]);?> + type != 'normal' and isset($branchGroups[$product->id]));?> id] as $branchID => $branch):?>
@@ -163,13 +163,13 @@
execution->PO;?> - + PO) ? $copyExecution->PO : '', "class='form-control chosen'");?>
execution->QD;?> - + QD) ? $copyExecution->QD : '', "class='form-control chosen'");?>
@@ -177,13 +177,13 @@
execution->PM;?> - + PM) ? $copyExecution->PM : '', "class='form-control chosen'");?>
execution->RD;?> - + RD) ? $copyExecution->RD : '', "class='form-control chosen'");?>
From 7044ccbf2caf12b7d6e7dbaf273682cba3f20103 Mon Sep 17 00:00:00 2001 From: tianshujie Date: Thu, 18 Nov 2021 11:17:22 +0800 Subject: [PATCH 27/42] * Modify logic. --- module/execution/control.php | 2 +- module/execution/view/create.html.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/module/execution/control.php b/module/execution/control.php index 1b3fcd99a8..2a8aad91f4 100644 --- a/module/execution/control.php +++ b/module/execution/control.php @@ -1398,7 +1398,7 @@ class execution extends control $this->view->qdUsers = $qdUsers; $this->view->rdUsers = $rdUsers; $this->view->users = $this->loadModel('user')->getPairs('nodeleted|noclosed'); - $this->view->copyExecution = isset($copyExecution) ? $copyExecution : array(); + $this->view->copyExecution = isset($copyExecution) ? $copyExecution : ''; $this->view->from = $this->app->tab; $this->display(); } diff --git a/module/execution/view/create.html.php b/module/execution/view/create.html.php index 95d89b9a96..faef2a3cba 100644 --- a/module/execution/view/create.html.php +++ b/module/execution/view/create.html.php @@ -163,13 +163,13 @@
execution->PO;?> - PO) ? $copyExecution->PO : '', "class='form-control chosen'");?> + PO, "class='form-control chosen'");?>
execution->QD;?> - QD) ? $copyExecution->QD : '', "class='form-control chosen'");?> + QD, "class='form-control chosen'");?>
@@ -177,13 +177,13 @@
execution->PM;?> - PM) ? $copyExecution->PM : '', "class='form-control chosen'");?> + PM, "class='form-control chosen'");?>
execution->RD;?> - RD) ? $copyExecution->RD : '', "class='form-control chosen'");?> + RD, "class='form-control chosen'");?>
From 68f629210b6d25ca3fe5444acecd91c4292f1867 Mon Sep 17 00:00:00 2001 From: tianshujie Date: Thu, 18 Nov 2021 11:23:03 +0800 Subject: [PATCH 28/42] * Change picture. --- www/theme/default/images/main/tutorial_en.png | Bin 62082 -> 56759 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/www/theme/default/images/main/tutorial_en.png b/www/theme/default/images/main/tutorial_en.png index a1dc425a907d9e1735af0817cb016c8401e88f2e..9df1035e6937f5727dfe7da47ecbb05c4a141d0f 100644 GIT binary patch delta 55347 zcmZ^~1yCJ9*9C|KcL=V*9Ucx3ch}(Vt`E1z!=2zRfdmN-!QF$qI|O$KZp-)m|4MD` zR?Sp(ch#Ml?&-ew);;G0@4+^;!G5Do)S`w#B(L@Nflg+GDNeqE=1!i1@jwIs1ULW! z0RChpSepO4;|t5L&(Fyv$;~Up#RrfA0Hh@N#rXK8_$B!HCAc{GcsO|}DZCwBl#~P{ zoy|QQEuGxuB!wtHMp!LuEd==g$5#6P&o&wTlVbhWCsdeZa(EtUc3w*hfH|)vi1t&j?l@$jkI{;v6#${=m>;q2%$HgVU&Xb%4pJl|uDJ9O%!zuoMzJQNc zl0%ALl7~l9num{F<+YT{Yml>dPnD0W-pkLIHC0qRCQt1w72JegrRzCo4K)pq$}i^q zstFEAJRWA8IW6&^0BA*D& z{|(M8I{1cdqy=T8VHzWKnOvbXy-Dm#n6}&+$NM*Z_BQP~8g?3KzklZE9NG-Q(soZizq#;_1cVEPzPDGz1|m$aHSmVT;J`}V*Y~qcgfDWyCf^`?YISLp|j@LDcfDc z4=||+pt?eCIH3Hs_{ty9vmAMTyk#)s6c{%NdS*yB7$wade&S9~9XZvevcPbi!%u8cV0!&`z*6(jJFlXZ35um<(h~M|;srXVjZxys0PKSY zZJ=%q8vxm}E&2{6LT-L5q-TWcdgM|OCA2_bQ!&jEZtI~Nz+RqjjVGZyeteie){+q_ z13vDW@X4?5avNGQtuj5OkEwTvJ%}%XxV2VUpG#Grw5&Z^sW@c1e|sG>NUJ(=zV|wJ z>z&@rv~WGo^R6E`VvecExtv(!2VeA*=fr!u2M(=);6d}5)W)Jl$_JFd6W@IQ`ac;s zusMzg8LRN;poOw}G6J_JVbCt>Rdwo=ICJJVzap-`qpa~K6}I?q*|G+ zkjo%B1aoqrzGB2gnBs43q2)OeHMz9h^|v&+I*LCqU*vAtX~AA}c^b65VA(eK$lV1> zPV`)2Sq;vANOycShb9{u|GNG5^|TP}t>A?5ubG7nEVug-+A_zu+b((v(jR%CjQuKH zHR|zN)fe{bgZ9=UOq*AS0grn%Jmj9AL_EJz^btJ7<9DlDSl*j7R6sRm_c@tK5_5}ePuztUu^t4 zhml^N+ivOgX1br-_tLFh|1CS)p}-^uzY6(IUE3F+=_Q5hqHA=Qp@HW`b=|AEpvy#z zG0&ZTOb36SnY9#h12h`wU6NLe;w|VuByIks&o+4Xhp&P;GmdUF9c-rU9ow|>jT6lM z2kMJuO4Y#cY!4Um zMk;fev5=oZ!#9?fRzTRBPm`9JGjK(ruGRq&Fv~@LYPk`qa)dO=oKD`%p&35{Gs+Du z98n09J!%6O>~C%Gy}+-ch3hV`P5Ny;+iIRjqaN646>q2s{xoNQI&{jJjd{PM#8MsH zDmEhit{MhL>}dc`v}hFvKU+Q*$H%A5iV4C~rf*p-Y=Nk1Lpyq5|7OPi#BDw}u?p0M z&6Ar+w29}uXFWGcNZ@%xyYX$(lcT1RvdLs8eP)vBMGP${!2#1d0wejK8_mUbt^Av> zF7OcF{paSrsRV@Zc?m&f_t%vYIUf62{VfVCbOUzj(H!MBS5!X<77{*J3(L<>v}#tj zL(wu=COi`0-7n*(oISeb3n~3&HR2Z}XpJG=TUXc<_Uklat_l(|X1MW4@4#Fzl70Q7 zLve3qcxmd%ZwWqFc3`>4MNmqcv`m^HqPkp+#g=7D^;<^?nLh7=>h^c$p zxpdlhaQ{w*-_(Vufnjp96LaVRokctGrOMp?hP$fghab4vD)=Yb+?hA*td?+in0UFh9JgLYRl&?FS84CLI9r0WOg^TF5=`;)(?7}#Wz_8I(3)5 zKVVg@U2$36pLLhe1)^8_`grEz6(;eL;F#g=0hRk6 zJTe`&WVh7z&NO`}y-p3B=n`kA1{SzKvPeQ#bknN!&rq9ImMbYq%sQ1>=2Yh&SK5K?;IO1t{8;+=b1CNy-(LKpbVt*no71vNz^|ZkR6pNLR&$=6G zpLq*jO}DY%4{w_H;M{L%1G^ylv|!vbNS%b8bv4g9o5IYUKZ?bs~4&*$1xcFgZu z7>5ga!C~VKpSpK#aSK@H)HZF*7WH5&Am=#D{Qd2QaZqsWeGGlyJ=4mLuYakIgL~&D z0*{r9xpO7PGZyC~yQHe5h3$Oe#af9n`P{0c1^u4sj90JCrOr+l$>V>NW08N{_eU7I ze(`-!ojC_Q6kgL6%qXkdF5Kb}zbFY;?(2fcI%uI!+C>y9yH$o>y4`K3E0d--g*TNl zSR>$3(->B>On20tAXmgsrYk035^VT=Jc)&x?Ou$Y$Tah-5Rty7`xiv=?0+;wD?Z2S zFKaT@>M#p50#=h#n5avSD2oQ1a6gbTG~&6{6T6q_+0B!}89Y)h@Nf{cF)6*&X{C|` zG*%9%N<72+*LPeD;+C$??LO_<{V<4&r_?U$&8XJ8{IR7q-%RJXpC$SPCgcPuF~SZi zj0F_Mu(Yw7l8p(JU^4TPTN7sd_0fTzJna-=dX>0nB6}gW`ghj50@{dijA6wbu{bdf zG7^L7{5|jase-7fXKE$50SGx$c7*+HdkfndAGqSbpSt9>vZsT&8cVGImU?3M*WZ?K ztI~wP3lcANgXDeWm&~CIo*iYc#~*u*;k)X1Fodda4!nvY8Z;)T`8u!VJ7%7s0@$SL z4_ZGRY+&i55k4A=@ep{9F;{ohKLX;fIC)Vsfsx9SmR()s_%ji9kMf~1nK$3AFAbhi zje>f9Y`*P@F1N@m3lTK71rkhdc#WA_u31tH8{Wc3eNRj0nM!2)@PFN-hRo@0_!9B0 z?oMZbbkAN}<37vBUWw#wt0l$4VRxbWdge7l5!T|9g+f%5=!owNqN~l<9t&`w1Ey(? zht&Emuv>x~8PCHA&r3b?Hw3`5@9F*MY!XEkDl>|nh3t%%3V}l;{Z4Zw>vH6YKAG4_ z27`AlK{=GsdUO`JoAYe!)Il7X2nAYG__WL>3SKxD5_{(om z4(=pP~o!R7h#xxmviyX$^4{kF-T`{a?e zO{9o(kE~qg@-!VNV(aTOH*xM2zjomI_vUR7Oxcar?)x~@;}L9NrH7Mk(?|X4cvrs; zS+T~Ox+@3&Xo2hP{|1L?eHX|={<)T%1kwDqO*l9*v)|*>rS9a$J9h2Y;O-?(kFl)& zZ?PC;-nmdY6oWwX;P}@1VDWsXlQPg9OpCe@M$?LqXnAjagLKXw#}c&Gkl-mc$s+KT zi7?%uuTB$!x|~JbZv z?fD6$kouGN5>6|~o3d6ar$OcJ>qJA4x;`~BR)#ieV{4#Q zs^Luhnr6#b2hGkg-FatzJ?U!idI zJ__C%sYIEycHbr5M!t5`*vHytwEog%t_Pci`AzSfgC_TA)1kfGLRD>b!y&RCv|?0VhNMFfeyZJjoPaHgmXI+H*qUTn4sC zUxvodf8N4ANV{D7ZirJF)C%4v<1|O&>Ufgx3;jUU{*tAk^tB*nU%M~fx##6GF7`dR z&cAlb(3agk(E89y>hL6iGV|mBh`I$NAfM#r?w!YS=T4f3j0I69|La|%Wit314tJpE z*C-dKe?nv0;gG3gTLZPM4gh5z-~9;fsJGH3%kIzUb60yOt8tORMjPW`>aO}E!#pwV zw-03B@9|@AbEaE3;*J=h&~avv7$*UveJQN$7~D25Y!+CR3bE~)$H0<>9S zFEdJcV17v{$4V(r425FQrjl9i<08G?Ru&9NAg-pgxtsc0O8(vTNR<$$78&zUxGXnF zCG)^6261m}C*G}7w@MmLs@cthZ=xNN-LjMB3qzT#KL;W2ELYn=)LtIWC0GTU1vun! zY?{>vAHgfGdjCogw$HI#vyu`;3*R@w;WovY{ua)KQplSs%~(pckSrbfq%7C%S+7(K zNjv8n{A%+s6%4hn1VqzE^jA4u5BIR-)}7Q6mhbyFjoRT8%;8C`ai~BH+kpAwpe#O= zWuK+oF>&MfG*Ol`z}*3)zrqNvAki8uH;DU~pc}W_Edtwu)5+%xuKezS8J7ax%sJRWvNz`wu9?XBP^inKF@t&vR4 z=KEWo9DxSZ@c@BFuMBoYn1jgh`~0ubG!5`}xNoK9wJBC`Q zbS8`m&)IUkUehISjkA>)u!i|f;9irp`6R$TPcG2Vz zMD392;J<4Z8|Vb5goQqo20r?K@H_)L5w#zW>6}B$p^A4Y-qt^tY@}IsKa8ijG51)t zM>zMc^BJmU2VaontYngZN7R1JU&pG_5pLj2_L62#_k|4+(E)$?uy(jJFm?Ze%L;?B zq?u(#xXWmz2d^OzaIWBbdLg@=bc9MSQWQ-)z-~2fQvjV;5?km^P;8xg9M>#tx>M#Vikmq>70+5) zB{*r*@wQuS!Mm!`?$49@_cQIx7l)MVD@5Tu0N0`Bbr4_5%Hy~?5M$p2POQDO`n&oN z9qTNhizzNa*o;Ey^>}v?q2{-6l*+9C{6<&gj3vch(c3N1exwX*I;b6iiK1Tikd7n5 zuO>K?fqd#h=c3Z{9KngIzF;oH5Z|nfP!$iTDv-p-fF(?Ci}Uxb3u(UPEK;Cu_fVcsVCH609@7>Z+AX8pYLq z-eEw~9-F(|h;q#iI~1a4LN6+AN7gq(jSZwq%wdV-Gu}%#67w*!{HT>((uf<~h=(Bq}sRSL&-Pmj!&RWi6;i{GIWH zh4^>o3pYN$(65jES*z_$f=qFP!UN zBksI{JeV^-ix*ZRuAX3+9REvPT3w5};$oP{Q-`?X$V{UwwRMp)lk^u01=uKr0e(@V zZY)A%jhPxdYp?}V#jRak7tAB2{OC{D_lr#qzGDJ(;uncmBMDq%8RIFG!d{MuS6TA^ z78E~v*1AMW%ocnkqQ6&sWU;DFx7B#ShC@OnfcBSgNFJaYjxhZ4qGY!GJM3qvrQvMHQjbHC=(le)?+75M$2zRwm`mECIdL;m0Trf3L*&JEef;{pWS>);}TN+lm~0IR1S^;t{g~|8FwnBg)#N z$<^BY5!l;|5s#FMOWbtP%Pum_*C?8buWTD|?zdLwyex54j3Z@T_AQ+m%s7dqAxrMf zDMs!AGURcY$g8t`pw&=P$kGH@T{HN@@m5cuf3hzqNhZj*6T3Q8Tbh$hCRdVYcsfrJ z2xl`;wKvW`9$2Y-o+997zYP`micS2wwL`Q7q4le^-gWwSo8|4}$D}8ag=Oy~ovsy& zf;VU4UPt!4cna6UiVkmIw|2nnNY(Tlv3T<%Dwi(yw}+u@y}2U7VMi^P*A#N|$7YGw z>?cE??w*~y1%b-~8Av_r!?YazTCDM+xB$oD0wrg>bcnOnkUkBtG#Jv2Bx$*QI?*@!!%R3PG7&ddzT#Z`gNu~XI%-a9!XJem4d1Emmi^3XqjOg zPJ4F$7TSpROf#~SUn}A0b76A4u-S{((03PW@Vq^_Z@UmxKaa42jrBqwm|&~gW1r56 zO!RZkD`9ym@Si>7H8%Yl4Z`Cdtty+Fn^3SUm>Z{rjOAXekB*gk<8Me^F0>Lkt!)5C z1en$y(k;l==F=8gs6nG`yg}pTgPq0H6FC}jKX+r;zpO{b5qp`g%`woUX%_qhp-3xb zn!st~v9m^Y?!(%EH=3}3S{2>N#IQ>%_`zcDths98;EqNlrd3734pYkL(L%naT7(0Q zwCqBh`i$xfPG4JymevmU9Uo<@a;#wNjtNw43glO0ywUMT0hAarV}dz(VSmTfjBR{B z-ET)w#~v;pu6cH9C9~Fmf4p&niz5#I$2X6+)~+@_8Hfsg`Kt*lg8N4+zi}o*uVgoY zZrb^0#7eVyNz_65%~%>?J-1D9_VSfP`J+1Qy-?0zKyP6uX~s=Fecr1J6u0~Mde=!` zlV`T49@kaw>jeD;70+i}mkJc`Hwd zcHx4%W|M20LR9-OSHYwGAe@7l@>{$bAi7Mxaq7p@DlUV4gPld*Vzl2xM(Cb+;8rV0 z%~=3OnXgaYZP|#?E4SpV{(giMB3ZX@D)Vfy3@;1bm&}9@ zx%c2C^*VLt%Rhp77cdERZo^yG8o&eSEL<88%gO$qm`P~s+z@9&0Q5j2MUqlmt(`SW zFVzsv<+06LD;^u`W@yY{Ldm=@LL-YGRJF5>XcoayXFNMWZNh#CX@)?W*4ePx>2c(& z|6+(?TQc7t*yh$pG-_SUYW(pUb5W~fAt`Y3MHM;@N+f9pwmAWGJ5%;*jr&*Jsyt!z zXT&mNfvM=$9Ym)*z_9P2HBpuzT2&;osTUCN`1XhNBdaP{S#C(OO!A(XfX8I63E!@p zg>9~b&zMS>PgE2^U&2|wgU7-51ig5>LXj5?w40=^4+sc9Z?Nata9j<=^=l=+>Vs{_ z4ql^&t1^~RQzE5m=4flhM@EjGiN@hKwMZ8y(RcemkV-1W7ymfh0mSM&+-~bhL^Fq? z_%`jKO9upKO0+O}$X>ty{T-%GBVbIV67hb6qz83~%DA06ax<_CVRK_djh`p>B8Tvk zg3SXAi*TVBDO)ArQe3@HXi<{F9n9CYnLL=42G!}I4|p@rvi}9lrd<@J3w#dU6x_oC zFh!ur&(&N4@{)9)>jWQLD|0_mxOT3fK&?KfRDOTdpB7^6nJ9vwz{T69_}#HsGHGg}z}uwFDnR(O~dz7f5CBw5Fys!yl~l2X=H2v@#0b4f6l80n$X z){uQu(RX0DN**8Drm!PfXS+MkOv+GzzSncryt_QF=hW}YN{h4a78!&!ek$u!IZO_N zek)O8FBgwQ!Zo6Ohl*gd!&6loZiQ;b#!fyb{k*{+GU^$3u@GE$@ObJcv#w^wApLUs z%I5U<^2r`#WdO$dV#Xt2KDi}7!%zjg) zOS>dPZB%gyU$rTNZStM_CPUi{M2B)geFUVOQ%SJcWJ7~3=CTL37-^;CKfk+fyYfM| zL+2}?F8@GaY95{Vk^p;-%d>E@h4jZU$bXa-N$M0gZ>iF1f}fr`-#49>(VV1qskZy6 z4U*0alWYy$Ft^mzg1;pD>Ocm45y`kEsP`rQNBlA`VTK1SI1cCO-0;&v$<*_{#~h?g zrcy5-Tu1!32f1(8j#{6+lL4zrkaJ>PgRr7p&8PuX75$u-rQo9e>Pvn8h!e|iEdIDu zt0UkFL*(VucmMW)%5KL|)wS)f7*#n`ecm3UB|M%XVoRyl+Isb;m~^S(SZqx21lz#l zjyVhVSyfjZ95*5hl|7Da;4`q-{4}{)@Aitbm#krPtPsFe=3dM#$*nK3Qu@@1lE5H^h4VN^&;9UNi)f2^yyDq@fc*S(f z83fz>lQCIZmbuxXDO=C1P|mLDBM+p+BRS#5is3W>^Jx#^Hh3BlFA{ny7HIwHkV>YT z`JBvru->Q+JK0}{J_86f)g-Jp_{MUk2p8alYI3wh!HRu;Y75)tGuGY7i@Jt0bNM|x zh8-l2$eTiQ*(Lp+)!W}Cl-(V|q>)x0!Skx>qj0Eu&w4R<7%rS6ZhJv#B~&OFTQ=Y+ zLN0d;=1r?ou)tlCI76k^E-IRsM-)EjwZHp8|8=hxoro$e5%L;9S07g@`Hmxo)vBmd z-uPRGuu&~*uHHA4$y&&z2GyqcV!>eq{4h~do|{^(R<$CxV@I9j_w!V;U5^7_f!r7M z>_U({l`cO>Xf%HS#o7J)b*NAC&Wr|oV>2um{Meg*`C4Mud^SNK7H|f&z|$BMQC?#+>ZZuojG&U5AA376YeK61x8jBZ;*2shK}wr8 zwlFrz;SsD}!>c6%V{4v`KIcwdY#npb1J)tRrUgpoqeF=MGZ> zK!>wh(R%#6GU;3!_FvDE%0}x~lEEZkXtOBY&+33%H{ZIj#p27|hN?Z#{HMt2K{>eA zAMhHkW~(QQSX5D();EXtq!b$6w*89({Nh`T*4%++dpdI(fkkx3$6Ki}lp_;pWNjyEPN z5saUD8wCLJ7r9X{W4vrsN z0O6~RSwN$t%xvBBv?yA@>O81)9PQ;TF}>FGT}<&8OK(XPAE|k$oy$9N%G8_-yHPAw zGM=fa>!%W-N8aCXBUd8k8~No`+l2EVwjqcIt)49hBf8(c0`%|Ty}Pc^fqGOwmaU)e zEA$<&W|LKmSOyX;M5HZ3#@2iq;QJ`niCGsCJ&f2{*X3b>eVd3oh)Tu6r7O&4v>xff z1oBo(eX#dlEQJvtkPoVTA+7(A!byM=szWlU5y)&0T|&fP?Zv+Nmf8_{Y(*G0y{0LL zqu`Izz*PGBT{m`_EqztaV9{j+{PQQzPX8ZBmX?&IcEnXCmCX4F?jQpYFK=ebSACkPuxYBbdta2 z-alqw^6w@KCbomGREt_ap1)ExH_;~QdroS^88}j=(tDgbcITb_HhcbL1Em^KVDJ3M zae1GB}1yL7r;9**85 z3Bs2MlG3r_T%BmO8&+AydH|3*0i9y`KwUa{U;QXq?}E^?8qkRYjHjF*M0iE!-*$wn zHvs$2e~x2=mf~kkW+{8Nty3%LXsgxR(^n4T3dZ<(v%&y6I-(#7u|w?R!Kyf#pcpf z9$T#xPKKX*=`A?_g5NBX0Xlfo)ig6Yj({hqPQD0zR_O~qmf)DmNdhR3rC^FNKdZ#P>+gaICCz!X3n{J%(?t?%S@Y)UT)_MtcTdwW%nUQ>T zr5~h(33Bm@)BI>)pNNnwpsmKBsyt?H$-BoHcmDp(o}IL_y~XYzCj_(o+PyrtLx=xH z3rK$6gA7-e`(@oOu-4&AC&wGlTJ3@LlKX8O!96*750As(%Hp|<>z-nY)|h191YMb9 zS)%<20JKj6ruLoHY1pQTt^74mQMPNdAy;%*`+W72bTrWldQJFk*1N+(>s&syB~ATH z);}#*he)D~CRG_~6Y>dm9i*86BzeEx6L{Z4%d%|l;Y1f??@iwZ5Mby|6JA*P>>W}| zG(liaOjXk2_4yCvD-OxY9cFgD#ZIYzF289j(QHJ6wKQr z_73q@u#h*mZ&1PFRqU}zSka|Pn4sN(D6UfYS@)zc3UjuEiM&H$NDyBdB^2xTu;KX?+2fJN~hRLMUn*Q4=h- zUq3gQ`1*4!Z2a5ISYB}g9IXz$6Z6Q>Sw7SGwU}AX^(s|ocxQ@wUBQs124m5+cnDIA zYP^amIe^&Rv0?QZRz&aZGqZ!ql55rZF}hZKYOtzG1G3RGnlGT^>4r|cceF@RxK}+H z!QV;(u>EEpcAS&FzgDb$@`c~V*+yQL*6D>2b~4k~>kD;?w>s%Q9y7~*dLE>EzPufw zcgiOe9Uwr9$x?rq!84iu0X1>LGo`ESQzf?x&Q~xWJQ6sT=Z>www017OZ1Zc44E?;@ zg%1bNh|O(=tiCTtTnN(8D!WSa>}5mYfjzShWl5rk*XTwh0=zsK9dEbCCkHO`kObZU zaXMfrDD(S&+qwGO{=0uM@!~K{`YB1D?~12+I3v@vU=3SOOdiD(94+cfwc$RhLjqFnKsqt8UwC_QDfr1;nkfwcU@K?!h}bloNbHPG@C&t@ zL2mD-?w`Sy4hUpx4*F@)4`VdHD^}~`g=hV^4W9>NYT(1QG2wWz7^b$OT@(6)$F8v} zWQ|*#xc^O+8ekIjR-8l`3rlNCG>^SYQ;gOrHy#s@aPAQxEQzEM1vEEYRW}#j+V)m+ zDT+50XW6BUi$Hgb1!-{-6)8lagXg*;^OT-39oQ=*+;Q{}-?amv139me|Jfz3$*st) za$2ER-j46Iu?*?;YazAAQ-C(G2M<0 z$#il!I4+BCN$N&HN5XQ64s>j9{Ay0(RZ#-ZJdCp~KPZqB<3G;bQj&PmXD1-6A8wk2 zGxQtP(Tlvs3<7jOi;a@_ZR7>51QxU&@9EDytt3bN7t2F;=52(wK@Gw99+-iV%o6(O z%V9-mzZV8@Vp9rd+r2Yu09U>76Ex;m2mUbce&i1(^vB@&+36Uvt4~|`K=6(kXlf)2 zZnAvhA2&^4Zro?j>e`Xe27<;ZA`sP^Du{vA8H{&vXaE^TAPRyVhkXV`K?Y60OL&A4CBIY#LsG53 zv*>JZGnM+dE6dQ+T&et~H}rG@X&=SM#U zOm|A<6oyKuCHz3Ceu2oOpsJmn9@+K3s&HdZ|3i0saS}mTwJ)^z5noO_jK@nN0$vww z6(2%MS*SZ7Tif#i|4Qs@klL3|9x!+s9juVRahte8Yo7Z#5w_l4e?KI*m!y%zLC$r+ z=M|@PAg&6suQZ5_2jZcj3YW0Cl5100hccZ<%hQ;rw7Y0i9r*0dPBw0?8(rHc>1M{cgJtC+hOcqP3GE}knm>U6{*9n4cZ5PKn2vrD zeHhIpvPy{(tt`KSW{^vrV8O{7Nduwy$!q|<@mEM9o#f@UgkveQ=>lH3NjCsylNAfp z1lO1vyOa)QGHVeVvLr&Kw?Ci)mS#qXw1y62M)n&iY!_wse;J=Uw2ol6Q-?a3Nnmr} zsVSrknnbiOae@^A^F{GKWjKDHXWD4-`oscW&*a^8Wv$D_5dh^QOprJQ8BG^ZZ*t~) z_nu#@Z(1r{vvMDqWS17$BNHkFRVOBL_CbzRd2GSH4Mj|jc`_1*i<>zpbv;_B$=uwN z+JKPi`yvD6)tfV8A*%tOy_OIrdv1g79^`X^GTjS5dhT#~{@?D>E#WkE{t&U zg-dD-BCC~iyocasC_HPHuJCX6AkWmb-Ryx|9(z?d4;krh_;nzoFnXW8YhZ5Ffuq-{ z4Jec2yeGb}LV5tATY)*EHcrLe@ibvZYg}}*P*rY)?lYH6wu2B0ze}F1>$rR7s4D}P zP$rRLCLmSHbG7WNR%xf@HKJgyb64xP1GkHdzY0{a?M1~nmw^GS`-(NdgZxeo*hTPk zK88p;MjF4nDlny!0U$nF5g=73Ef$jB!-;6S& zyOXQ)QUR7I;2cjMUyW`4F9oGR%X9RitC#Mv0q^Pu#fk*Gh=lwCF7t~x7Fo|qMr;a;&3L*pH!Qq<)ap?fB1qfMn9VHO6J!EC4grAqou zkAa`~zAoA-Ot;QQD7j6d#e;dQVT4t*KBZhfO^1%-JOFHsT~v~~{l+#4?tO`ZB`csu zvTwczak^Np^e1C*V=5poTkAiu7FwLK_>dWO^MqA*zB|V5N_cQzMpRPcV}iXn&#GkB zi)8sQ3!WGpo>c(3eTI6uU$x#5`gd>sP7PIsNNU#I`8C(C-l3WW>^*zpLnGWLh%p-o zEX_qYv7v<2ZCA{aIo+@2 zQq<|;gIeWw@bsVB0U;IpR<%F++b^~;VP`K@6k68&3jWdg40OS!sn5lne!L7Hpwa3U zqhEN+;oUaTAEPbsVNqM^K+L#;Ob3)sV*T2=*?&n){I9ir&kQAr{-rSy<+w%oZB@)g{*aRm4sY0Bf(1qWHxP-zn z$h5kO{Lo7@#Yi-lMme(JQR{&7S~gvq4Wp)q0h`I0mxsULlROcND7mgXf$K?`)Pxs_ z3ijI=zb-x%Y6hdy;)p$4^eDO^lT|FE6~+Xf10$R`V{U$Ylqxsh2}t|p(_}4x>T*{u z-|Wv*iZQ~svFdZU2!~MNIB zhR+IaCn8e7WDC(ui4(UsXG;39_s+)oF>p9BH+bdDh1*~eAS~}m_iMr_h$Kfp*6C(u&Js(u(dau_fw*rcqRIxX{UvJ@G(-ra8g-Tuf zl~W6z0i>+>OxaogB^q-<+PxJ>b5XUk{vcST=`#A2f?6aE88m@?`e!*s*un-4^+4ma zau{pDrW^@1VBgQY)UrLoQRg{YA@xhd@?22-XUQrpR%?4xzWIp|H7}~g!%6!|-k}*H zw<4ZXDx;q_X8TrQqxNPd)_!F%zJ#?Xl|&aSJF_8WqiV`}(V0i}MWFlUo%Q?=(|O`v*tgE-_U z7aEVnE0cb8k!wM$oDa@6hK98dL1Qe|jepGT2c@#5hk!u*6Wv1_(PJ%#NSvgwHkgXH z=I>lW#oM)wxLel7(EOh!$D3v%pEMziS%e#_*%_{k2vXGhDstY0oiK+e^k7#Egd4-R zIHFMTFOLcRk+V^sT7i(6k!i$G$41<#)Yai^$<%Oe?jF@X2-U1-Qw}fk1yD|+y&55v zn<@Zk!;vX@rLs9_XUz+AlTp6m1j8RUv6u)h8XNf9v`F}+vh~hGow!K4jC`=VC>Min z*{ChO6n$Si_rmsp-M7=KzXqPaFb1x&JDCVNCAJejw8r@k& zP35N)1L*P#QOcmD2z?|$O$(Ex+l)h{PT#Pq_&`Ek%)Pk&25~0oLNfZ`DPxg3E(7U~ ziNJJA!BH9_7+nj1^>S`vb2#MD^IyiN&g)mfp1PG)^TQKPRsLTD@+l8dWM$pwxz7+) znp=;HiA+Vtkn#!|!QDqH7}u`d#rE@iP|Q|XdLrk=X#En_jrA`7W1V0gu|Z)Wxi~I} zt+DUgKBrkbV86E2LmLS^n#^|J%;ro@Pfk_rk5y$+vF5{KF1-BA?vYl)c2GVk-fG?X zG;Hm)qkGYZKh5USjFeG)1WaU9bCXpo{DO&C8hx3=F8C4t=%`eRXjqpof)fDWVvcZ0 zwxjok7b}lJ7xTlEWG$MIx;j6}UuMj!G&2F?->3{)X8!1l_+NhH|4=O>i%qAj3+Frz z+wcdRTMGOv}O6hH)IyCf8$)>d?KfFlFtnS0Gnx_EUXDW7kXNSr~~c$IIZa~ zH481g#g+oBI7b7$^4Z2S{-WCN2jt)QhOb?$N$cp^@*!PXJuWb(DP>zRA;BQWTqK0C-s4K1a%mwAH{p4n`0rZx;cM=*1;SL z@q&;0mIY(^kFC4Jezy4jnQS-HsO;ovqu2J_{<mYN?{|Iyc1kU!hn zHV?^}PBbPhiF?Npvpq*vyj&B-MX7@6jZ3c&r~-?CaA?e$d#gAEg9NGWSy-c{H>YKBo~Nm z?0YSz>rL<7dsaip<)uCF8gAqS8h`k%k2!=%H)Ky9#u||QQ|b9>|4MNBsTvre(}JNg z{Q8&1%g23jV#znLpXEJ2r;lqwd8Uxm>bm6npgc`x8%doi1z2E}Z*dO6-r(M-YWGUc z)bzooOLKjib81N1RYd;>uSyz6VFF(|S&r!{pF&ceq_{7#a9IksDMSiyw3^+$U(9vq z`6H3%nVVn@ncGl?AwT2y9J&G^bn5G?KMaK% zIoR?P^n9w1XOSK(h|>qh)I1~jB`tj*mMCpF1ZoxAbvV&A?&65ZA*uh-vkgzP%Ah?& z2fcxfi2F2vyHkV`s=EtFVL6$9ax%@&22OG)blG8*LW#J)XA))kDZbp@g2~$3fk7Co ztYVspGjdG-6~z^4$0Mg4D`zx=bm{V{FC-$Nc)G`-v*CLD!sKy`d@{f)42Ww`GDRpL3vBf9zBm#_0p zB{rfYMQNE?QKs1}&pCufN=J8>=xJx`{iX))*- zB(v7PxiI(@<$soA7sG!bN?L+{zLa<1YHHAE)H%I2uy~Z2yKd>Ap{Nd!jcVW9`>09= z^W%fQjkwE%(ts2cmYCL{%N+ZgPwPU()K5Xaq3*xxLH^EeuTtTtQO~>Y+I$ztLF4RT zoxJ5qT)$?yr`1#IHrSt*cvHuj!62vdf)JRSNE;_miF^?yZ(8`4|AuezOdPCa&6nVt_0PMlgALH7Fvk#D5>tK{-Q?Q)8l z`qJ7kmtl#i3{XJOdEFjDPVEE-7g#|gu7v0QJEs%sFNaFUvxk@Tu8_^m8(x&pD2YS) zzUag}j-+k@a~CTi`)NG$RR7s$e;p~Dqw<~26Ci^+E9z({X?9b7N!=VawOURLDR9IK zd`+5TnSH{(>ctH}5{hGcH(1lwQZR;2Q$=BmW3&|5i-yn4HNL(iLY0IJoTK+LJ(S<` zdwAFNTJGj*@tJ(kwuG5~$HT49L%VFF^nW%}Le8n3f~7$OK`#u`Kwu?(tSzy^?FYS9 z(P4KbR@>a>oLdct2Mf)%KP}`?>1QJHapdg}EJ}7(B%Rex+5ab3Rykh}rXL8Awd~n@ zJ`o}70n<5kU~yV2_K{!sA)-Cs6*-+|RM;<67$%hD`9K>A2{m8W#egzyZrD15IaNe5 zyPxxyzy;H-R&P-tX3aahRj-x(dMw zzFX>?_xSp+3I917NxPyO*bN{~NKqO;FW@_Pnei_rEZ!C}oNG7oFVphWmf`8C)WK7% zZf&;GZrwd0M0fK3)MUkf+&sH;)9|z@9KUh+n)*?H9OnIp9J@LhiFpY>wb+h9YPb%U zt`uaVpihU#nqduno&;Baby=ihX?XK@cGP9nx-&d!#!N9^fYA7LAEV|jZakv9sR0e_ z_4>g3FqkVMDd$=X_j~85+JDy%?~;`QGw@%O<-_#kIa%{iStH&>*V}r>!@_-FID-bQ z-aYtiB46i`@jvbYd%!<}TF7=bUVh+|Z@l-HZcwqMBu%gbCSr^zF(uT{Tl{C%%!$h@ z&R>YReHofX+qJ1m^qCoh{T95%DG^}T1zA!iEEzRhM9yL@6D^jX@+NDuS&}N+B-}V9 zcm_Ps!P*Ab@zBz~!%_!3C0#GD!k3-*6hv`4n^BX-&pUJo8HdZ(dBOG~m(KjQ0hi(S z%#BPIH+ZWV$MJ073EzA!(;T=zUjDj9@s7aztupWi=eLq?_(n$Gm;NjA1=H^Xxd4&l zS!UG>E3@OJ>0wy73f4OgE?k4^Hqn{QX2s#eCDc&cfRuAFT~`9`JvCy{L zjT0|$?*OkJ|7oP7uPsh~1Fz3uw0}@ujT|Fk1PM{1T!wC zXs}Hd$f}oalzz-;_5X>kv4XBM#r%Nn2=uY~pAU|8I2b}D^|2Zk$-C!{sA`*9aCT+j z0e<^tB~%!pWrNT(Nqe zM6oz2*9fo=&yFNUBq0cyvu5jam>==vs{#dH43(o4oNA#Yk zeW&%?or`K=Eta*C0H3+E=g>mv?)0LkIGUmP^yy`b3AIw#^TvZeZK$_dzBDpyThqg1 z9!lX{TM1>k{$3QX{v%O*5L5b;h*Z>`^xbydt)kL73=51Z%85pxhjomX7pt3!Mg6qn z8M(k~`o#odCdUxnz}M>Mpeo58eZ3@HVc0_0)(R=K0gT9p$a;Exr!x)0jq5*sq$?1R zjd7{}MgT1!p2(%=Rn-fzK%-SAh@w~M`6@%kq_zd*%lt|xj*q5Ch1d7Ij|=@tI$^himon6i*!9Xe9dhspDo_Uad0+G4H?L{|8@h8I(s8w2cOLcXtae!QI^hBsc_jcNTXE4#C|u zxVyW%ySvLF&wEb2b*jGl`qy2xd-u%t&dhdS-E$474nd^m!gk+Ys_oR{75Fs4V*M#d zYkM|`ng5p-GI-2gQ~pKYVney)|4Q3(XAiq7)}-)vTZpJID>5H;gbi9_dnb&u7*4MMh{CLKA-d!?xGKqH5 zm(=0P^8Eaa9Jws6x^vvW?IQ3YKmSJ5bmRrrF;Zdo&hYA=M90m1;=SY|y)IAcH_|)h zx%mZX4*#_8C^z<%wPPOw?eCI`$bqK@Pt>w9?6y0FEk;I4+ijzyo!t_hQ_pjK-j#2V z>RJw@h`d+W^L6Ag(@v^t^1@E`jL!X_)F+k_o3#+o?N_+$4TZ=E;ARnZBB^%F3_KQJ zN7EU1@)wYN)WIi}HUfS*!ppaxTShF^LRtU~IM4xVhD#QPSLYN?;8DF^W9aTJ+%d2g zhew#fh1in!j$ekpG{*{Z&-?5}w}n7rib?&RpYtM-ZG7ZS`<~GIe3039lgf>pvxTgX zpg-Qi*ym<+|6WfB+jFQ9>_Oy?L2)zDzZWi^OH~nt^gXAuL66Hy4dZ!~En-|;bygMV zl621XJk_8}=LCu5Y1r}rJRz~a;Vf*}@)z{Z@2I=1i_Ivo?koHWNvyEutucdgE_X*} zooEJEPv?WPTMr(NHCGh}U9?Qx9HwQEc#4{PY&e-k;49tWEqdoZMM`F_Qrs}S< zg0$#;#04}gxv+yJc@^%Sk*{)Vu?hm4D(IAoVTGnvL##Y4l+}YwB@*gnMFUkF>rn*- z;E{|5g}taM+E%94jDOEL-0`ZMwUM#bo(bvbqqgQeKBB&Dv0iqSt|QUCK)kzE;a{}w zagRb{`CGZcceq{Z^Riy~bH=&oy_LA9*{;K(GSyS>-1D z@);Z`DD=MZTQAtXWiy-kxpwN#yrc=fet*fs!xO_@fp6k6(4k_qB=d@8^1NP)}t z6I4cjJN=7Uo%D`~EB)>4PKtzKh`de&t4Wlgjqp)M0`(<9yecLXiiK%+f!CRa)(=n- zS|O&XkpEv97V!VpVf|mruu!UZ^`tQVw+u_M@A&#Wc;$CliAXh}5U1tK6kPpaNMDd22wssK+MddR{&o=x_3@dgaAp5&aU7FjKol_6 zeWpO>{5D)_It3a+sX0k-B0?RLV~HR-Am86BHKT}g)_-`6nk577fj}mGs&*2dMU9#_ zam8!S*tK<}tBzKGQ6?esM#x42`j;$-KNl*LRlci5{)exBYDVA8OVOy)jqSZwb^Z(} zCWG+8agx4{9UQ;cJsWm0=k)NgbSMD2(KV9HY=}%(o^_S%R{^C*xqWynw>j#f9r3T2 zW`T{qg$N;3XVv)*pZo>&@r{hpzX0rCO;ovhTGaYF_Wf*SXTVY6j$SJ^86`bvbYIsf zr?4TF;1d}&7r*G@L#kw9?BS`DhQ3L}t^V6zB>jV3i>vc1z(801uL4{y z2_fvU^QZ$Y&a;e$1RMvEbqje)kUpMNsK+#nV?zTCtI=nKA(vED{F(&qUYM&nh%!kIPgi?L+IxTyxUMw~F*4 z8z$t%Rzf^Y<;M5)xH9^DK$1E@u@w)Ywgx*+wM}@ans!<950@Na-Cmzw49kgVj@}*m zXw%_7$mZGdB`2ZRCQ?WQcA%*zothh}pw8}wxKp@4+RRho#F^^si%-ABkE1_={VBV5L zM~3;Sna*ptzz>0E>lfjqI@e`Y5)}n>&3@5DuMox|#^Aa|iHnHnkF}`i_o(n8daaposX(>uJ;ERk`>6Yf0Je=P!rZDBMVMP#s5`fxi|m z**xVWO-CfJ+d~A@0N)92>xI`vpIBV3sa6Z5$6@iUD`Pk7K1r1s7omp%gu(XG_W{M6 ze@UE4 z0@+X08qy{R9=xesq_IBh@6IH3*H0He8hGT}KvCLwDkQnb4!&2y}5#S4!$jxs&jW~k7 zrOa~nPQZ6)rW@b$pcLvCg)R*9ii|83$)2QM`qm6opWexRW1n3 zvs(je&CMkx=BmWVKAu{zdlk@}C_Ym&f#VvaXk%fT6@oF-((#aTD{X_PA|@vAG=#!t zW{aRbKAhok{pMk=mcOr&E!h)fyJo?lQNCE*rqks7`k^OocC4hT7B6^UW)T4=@|6!e z52M9aQ<_BX30y>Iq>vLQnxw0nn&h{UH54%*-%%(lEfA6w;!2@<7*BO^BFwRukTXzO z7D7yb#otXrt0y60Gwo~!i;J;NJXB%bTTI^9r(a}>CmqFZH?GeH z^hN*Nw*YhdB6iz;Hup`#jVWk5#wmodK9^k8{~4@}^OHYda&9%m){K7GOnCC1FR~aw zD^+5@@+`9{685lzrX^}Y*fZ)Y*-OB$v@iJcV#c`>^97_EeUtbqi$<1$q= zW7h2ny>6}7C3ie(^$t(;g&xQbJdwrYMa4Emhv;E>8{mB3GiccHNM&~DaZ3kYZn9ra zy;eksrZS_9suP%dujwMbn2}wua_Y}^oK_7Ni-v-3Y%!=ZMJ&lM*6SGfjJLPK5R{KW zS9A8ytL5e@43uwp;rI24l|cN5LH@fN6Q$pJw`gjHk={qUtbMcc6d7;9@*}Rk}6~yDXAP zf$y;k$^Y+Sg0F&xW{#cXQ8p=BU^K)TG$Q|91- z{;9w|LNWx~h7sYJ=N=Nc`dcoM zFjCb~3XrG|GvduP^coW|r@ZLTPR@U+?L7r)ucr@i7@pxQpB44MZMQpq6D?2u#F&?{ z)>fk?;MYXy#DQDpI!m>j-E!n?+vBN8`Y35VPd(57!M|N0Hb+17`B=>p$`d|SE}7yZ zDuDgh=3bf0rZ#>s93nLtErs+tw|>eVlK?NW+9&VWP_=(=3F7CFaEHFsA4;uvK#v~y zuIRIqmK`dxDU$uW1pXg?85eG6lF4Bc=*AA7=l7R?V9k&cczZWG4{l`1vf6(ztu&Bz z{}~1ShvWQf^gk5&iedP>?k7-XCzYP3UfDxW^HWa`(XK57m&joiFHeAUK#wov8t)dL zdB`SvlocTH_dwywB28}*->Rxi;nh`wWk(g+-xy-cJO^7m{M^yUBys73 z4Iw%wh{=y@I>U6_yl!f;2%8pUzRz!z-hWp_6I1v(FM%mZO^XR2v2{lGiSqC_Dp^8mUz(lkpM)(=H{5Kw)>8BwE#E6 z@t(n>cdHPss^iP%1RH$1B37UEE}Awu{2BTOI!BFtx75Kq55rkel@of@&Ko=_z8z`( z7kGMs@U_DO_!pzOtD|?aDJLUTrjN6xLGI&Mo0wTthtzSb1V6Oz3Rik;A$=bS9I(}_ z4I-Xg@*|}0XgC!7Yu#FbiM!~{dN?|FnO-9vdV^_DxFwpwAsbJbJu_;SPh)B)@#>>m#ZCu>f^N z1)5#bSX!2Y{~fp8L;u6+)>{f$pwbu`hcFJ`OBa|Fv}p?+Xg<4;n07Gt!sY+V7SNUi zaW$x{^iE9AVrk58qmO)-v@$XKUO~*Sob;o0l9oxj=VIOLATw{Fa^#dv(}PPswIDYG zIR8;6`gis*_vnW%>QP#GZemGZiCWK2(&9tdvxXf@H2}JEb^C^(a_V?2e)L#LWA6TM z0SJvXG-N}&X$F44p8B$Rx2%ae=0qW>t@xPt%3h!-2pk!*MPQZDcU9*`Dw1~7B^)5> z$YwthnRz-;F*h1`q}IkJ-xiNi60QP<21asU04?E&)YQj4Yp@nS-1a7NADNP)?qy<~ zWJE`_xcAkcXcJB#4*e*d-;)~+F$RQUT%vtXbiW~!l{}5jfITWE@4Tl|CO#m8@lu7@ zMDzCMl)D%t5OIF+Nz~T1{iwYWJ=?v@QDlU+E4?G{>~;H z)kR(7(_=HVWpalZisgph(ckujg7C7Z_+%b$E2v_nh9dw>4k2?8a-QxO zAj8gJEi->2(;#aigIPvCx6kFR{VF08ETPRNLRwyE^{_+!(1BQOV~7;!XJr?aic*(h zgZn{wC>&_vFHh$CPNgeJCic)a4X?Ve+&{`{!R8t8#lz)Qu>cGZ+e~34JM=fg785K_ zfjnQ-BNc+4KjrU8&ah#dvWZ(f_6WoN!_D>QXdkyjiot3dDi!{Cld5Sj&e}sm1WrQZ#T*woxxi$ z<`}aQfhZ2@4`U@DE)ud`*Jd%Cv|4XS!`3dCIq6?QLz|nsxyt;^o%ep1<@Kc6D0m8^EeRZ_ zoh33mE%kBqgvPUG&3I+o@+R+yc?f04Ti*p&!G`G)3UuqB&q18Ib3ZTuvaFNK*{D5D zT}U^|BpKjdt0+%zG4qYxTqG526Ca3(`BR78=q0)@Z!Hk-iUw)gE#Yn*6J)l{F>E0x zOHF~zKv20;CXFWcxhv^NMBlPqP(m&Y?R@14yf$L%Kig@tTQ5Zg1%V|qd$ANKqy{(+ z=kbqL<$5FCpRPndIhf?+HEMXXDr9*9X!i|3ZSI7g9@{Y|Ul_mB*ZqW`EF^=1(fz7L^`}Bib4u$aS$;8 z1VV@}um=tO?sgJw=E!EAbt`2&jY&ckJ|xVCn2R0<6VqAW0Wr>``30C`g@dD`CM3<~ zw{k8hY-w(8E^U8kD{=p3S`@O*jH;&VnMLU(KK$ftfG?4$__KuH6S0oiDbKcpv~Pu9 zy1Uyh+hO(FSDYz9FfuOE0`DdF$G^hB6*qQ`q<&(vQCWU?bk#}KIX(zXxDl@o>Gq8p zY5OPPpG&?_7i}aItso^k&X%qq{M{}#T83&5xCMD^#Fu>!By1?U)O%YQtfBbk&%DuZ zrY`Zoj^#SM+>JX}W6Tlk4Dzj3XZNBr;f{rQ8rOh$2Z$etE{Vk&$<%uDdSJ%DHxKYB z(MG%xAGupM$q2;bUa(z;Nt=WfbWgoRrpRHJsYWpGcN=ZklEdk_+a(2#m9}z_4h{dq zD%Gvyq_~SKk@bqTqLQ-yCI5+OozXM5r%N|5)ATf-vL>8g>b zSI=V;hGwFq%7(J>LEv3=NCH2Bmz_wQxJiK{j&-%EpQGN7I%S!cgy(X@pR|MOc})%5 zYd?s(Sf0#e27 zDk<@6yfjg+XbTmm*rywH#b3O`vPLI%sL z<@?|8j4bb97KMOJmheNfY>`ksekz00Q{*8AYbo7(DjHy4>IR9F_yYwlzM$~;=HFFeydy=>~2!SExcVNdIN3%712D2RAz>)(d zIu@lmde}?|!PXnJD)P<>OVP~|*;c^yo3Y&|~Hi%=D#)6ytOQ=9=2Tl2Y$H*2hTRuhx zURhu{ThIkSg3Yf0AKi@lC$QF%@L2_mA$;E``qJx)TAhjr6m{3y)D4gIQmMB^cKq-#0@$E$Bvf$hq#JW}rI>?Thf<)0KWxc52PQ%eS5 zUASaRfTGpxx=R}cs_CL1hfOmSW91#GbO)Nl`x_nbC4cncdj`g?>&hBw;C>+9MMXVY9I zrskskpfN(J1kLUreuE;(x+P}01KiZN0=PmW?%Oou1!g!XQc;q$%mRl+V>9u6xsH_7 zJpcgO?QFTn^evtFM3S*n&#QlYR5^RAWc(z(qRj1G7T#qP`D>vO;Gg?FG&tTt1=sx) z$+_mEwZL^^EHYt#&%!kOv^>qVY2}Ht=KzVAopN>rRlkmS+Pdc%K@(Zzg;%tG=?95! z<(M&rp_&+3OywG;on!^gO2(S&OU28j2)YBnqatUUWborvvXz4umzv6g{43ZSRYZT@ zZl>Lb#8Ttgkknyu3a+*y?-j)ET(aGIr-Q9fIpPwRJNX%VR|SWXG)RcCGG=Phwub|x zv6chIxc&{sUoVa8!v~E&0uXOJmsr96pR$8as#^r*!fNy@ud{B>|N8ptue?Hh(OMwK zHlVYh27c~$$W|Pcz9U}gJX9~F*4p}46r9l<=O;b6Xd!Y36ppZv2uYD&e`i$a5JSWh z#!DsSa?$^|kr1Y}qb7uuHBVf@qr6)BgZ|wXb~`{Z#TwJaMf{W~l?YD+NdJ9n<01=F z(f4!V*}1u%3#6PZTSi_&q~Z< zt>9kW1{ljXcK1{sq7NI4rQ|@M>s$TMHd zDaiB}(DNuZHekPm$;8B_1!S!8Sai6x@>_ku#52W+Uz#O@i9z&$8(sjy|I~b4T$8|d z+;+*XFTj--Zy!n-l>UH4TED{;uZ#3iZbU6b<=Mo1rn9P$uyR6JB$nhXRitfD{eeKkhr`DGWrt7QHnGhP^9Pc8@K4*r)~d9n!)Z#)ym{oWpU$a zcp~=Hh{KgqJa4T>AiN+T7~yQgp0xhHarb_xs-C;MC}#Gr?Ncv%$gfs!9l) zRJKQ6=-!83ZH_1*0rp3dHUWl|X(`PU6@f!@Sqv5*Y>3=`9a!zcDASiofvndmkQSL& zQ>QMYMq5#!_sS*czcHXnrW-UGp~{Yq=kb8?$>WVoLV=7SI)%Yp64J8 znxph{Iuh8U<3`AeJfzjW>6YJHzMdUp76|tl=UAx7HsA6e61*=mQ?;_b(P?o<%4q-h zs%LyWah4lKge@)E?$OanTxQq@yA(gHPj<;~c}k(mj9CG0{C|?>8O>o}n1e!q@#zWl z0rtjQ6hAiqY4{ZRc7ParD~wcEEA+n(kAmnj$Nu}}zay*C{`uz)l%GXn_4k>!dUkG~ z!wU_L3;$K>al>HzBYRi#ZLguo^}-Om#QYJAM}-)c$)rK(WTl`dEn#S6$JH-nf%>b@ z53$z3O4fm{E0e6@LS(zI0Ex)sFY=9T6J7Bk2S$;Ds-EQOKLXXPfkeB+$5;&FJ<}M6 zmqVYVG?oK!dz{(xRQhGPglH2O$XI)Vf?G!2U3R3|SfMM!U$Y=zix7Uw^Iv~oC5&&o z#vHZ!TLh8;aWol-!YOaTxllH6Pg{_)8@TZMu3J`zcwnSTA45%1AHeLP-MvD=!^fw> zFMEFqNW@y<#(X82TToYp?>i8g9o$NDoe&hg9rmRmLs;hvY-NgS&@X+zNXYoK2~gzE z$?tR-ZO0Rg^#v;u3Nz0ULY4P27pmzA#Og@ylA&~FM=Xy+i5WV1aUmphEj92Z{!xSf z)6h|9GVS!6nGRZjt5p=?uLzk$^Y;))kfR^& zobaUg^CLwJ2_geH5bI4&C4%rap64`BP*4M*q`K7*fEr)7{uqiPN_tLD9tdT`S(Zee zRPQ)0&PCLSkYF^95d*Sn|7h=U_|lWy7#^9V zra^7Tbn0Rw^`=D1w{r|(Gigo-1jzO1iLS0LyPQGwy!&}MO)@Y3ARky+lD^~1PBAsD zRh?jNVEZS#Nws=xzhEpuetIfRtba%3}OM z&m57*7cbU_Z+N6XX$?9rsbAY7u|A$>!`pPOHe0ZI7R+&n#m#c!ASRHe$Ufd6d3VgZ zzL+A%u+Bzl^1RqF=BFrZWFIhMd>R01p!#SE*syEPWIw1*r!2)@S@IDw<5q1}wfqWa zur>b0S)KNG$V|l;Gcm? zePrU2?48J49B!DmzmbhO(S#|+-&N+At4XJ~MajiNCs}JKas?#@jwSh449Ur#{F58T zfnZ(G$w6LzZbwfG?ui1pHD_@PX$PMG(Og{R?f>R0K2Uc3{VH{Bd`#%Lh7~0vp`yKA zLrAGS%om4y_xn&)j9(<_Ug{Zgm4da>^E8FOMqP$vhp?b_0FDb%%+<|TButy!*4`uL zV_oLQtj<8853AmOc4tq|HW*(5{$AiN+ku|)^~O`6LNdBDu=hd}^y{>;XngxY?_rK!PdJ)qFStfBO6r$TV0>Z5#n&r3 zYmBs0bl1uRHnAxcg7lgbBXjirx8Jv14-TyM@NYZwrkm|N7MIcqei`cG23y|{S8EC( zb$>b@8hpRJ-kEb{UXP+zkvlZQxLl^B3gUvPU&{rXhNR+Q-L*lFaqkeWMpwbrTY}0($XO0Mjx(~8~9Lwx!fPIrr8|=l{3y#}9 zG9)w zm=1v;Yr`YLQ)>1slgeE~TUiLjMm_rD>6Kgu3x~J`Pz#8hcDq8RO3q zg0!muZ-e{Kw(9W!5N1u;N~`DH&W^Hrp5a>^b?#c z7ljBdG+C`2rS665^~Kkvy7v9y;IH|FyXV^-BK5#xg%#j{h-pK!_xFHPUTtqg$E@dK z|8l9+Yfx%lUzLZCH;)PIwg9M_kFN3Sv`IZE4GNj3{+61MS7JLqz2Qw-^BgjWog)#H z5Nf4dFFiO0SQ$Hn?>IKDQEvMBgn(Rq(R2wN6ODBwF;3J7kqvcgfDj!5t?djw0WER{ zYrCk`^YPC^#zDc93LMH?Lc=fQur3(Gq}+~ckz2pxqujXsHj9I_y*5?+;=*I-<+?jy zv=}7BX)%J|8b_67F9L!J`QYECN3yQ{Gf;90F3-6O*syMg1Us~L@qWbDc{L*PbVrf4 zN0M#XV!5S$L$sM#3mNl+V0@^`I@#xPy)LxxP5DL5T%64GW*i~Q$tiO#2Y303rfXJKD`;?mzv?+RIH@Ndq!csQaqVzw3kqfqbh)%#D^)A?+B@(v z%v5#)FL<6zhoGjGgT1>R?_nrMbWxt)_S^3;b#yfRZu1;>ck_tz;|RoVI{K%*t~ftR z+Cxf~qM9=4{LYq7CLfZJY(IDe3{99Ii>-op+JSWc&4*0A`+GU+^n zGtyN)wu}YPsJKASROmu3^H)Yt zF`;eet)eEE7IaHm6~zhOjZ=+=hzk>r&5B~RR(A{duwjg{^}?(ObZ4P_q88Lp&IOK5 zL*-MnLWymB(zi!&lnnuu;E&U|n6p?V;t|u?Y2~UF8p8*EA|1DOkTu5e&QGU!x8)6R zE!B#Ssy2qFg)^|LmQKQW^eLSoQZX~6fBh8@^pqK2w9$YRb*QQ}AK{8aI{v{!$R}{y zWPbwL`jCt8!0$=Z%E%iu(fil#qqHFxZJ7BX@fs*J3Dul$jow zIsV7T0T%9eCXu&kJ9m77s-R9aIug(bJuh?7??HffH7cU89p<8ZHMV=hd_7Y>h_{G$ z6e=azu_^5MP5i1MbLSV?h9P)N7Rirji=aPSCF)EjGG8vrZn8&<2Nr$}85R~Xd+N#@lQtsR5m5=6IKFnlSHq7@~$h(1coKBsm1aDoV% zh+dM+BaRW>gDYS4Mq26y^@8&zqf@mSf8X6o{h}{>Fqovwtk?08e#oK*1=pr74qgb3 zX1>NchVL}e2W<~vpBPFO`e7ny5e!m}Sm%NFwW=^IVr>*1ye!7{o+!c>xVq9XhzhV# zKw+=c2zQ?~P}kv7Z4youq{ckZ@2X#;{*f7dhS9Pq!zcz;N*Eeb6*?uUXmzQsN@6p{ z4Zh25xgm9c`KK#Q!ECI=Jj@!k;=PpgDj`PT@{Mw-u9Uv_v#d zXVUUkP_X}r^*!`PPLK94&}pZJ2#F{wv@(Y(4F%u1xl4Ywnn!3weEy|14C;JY@>hO+ zUlWaZ@+1lv!(p)1oa65y`0zgLnrJ6+QPGv`c~#=Cv}oGT1zn~{2Di1P`o$*Wk*2*% zWK+l&lYh{?v_LJ)KVvex>-@02%EYVdXu`Bi!`{-QpF+Z@AYy$Yzz%5^>{p#1JGrn5 zJYyX~(y*MXCj|FGBeK84$5t2Ql*$7p`M=LM2Z@9prsfxJ3;IMCY~npna@jMb6(H-h z7U!Qhq@81nt!OPX~66B$kT$7d7EW@~BHJkk@P2K0N3J`*m< z!g=>Hg-lh#*Bbt2sDiW&w4pK9-LJjdST;RuZ%&W9_nJ(dssTuqsdD0{zR7>rv-|`L z!!K7qv_q)1M+jM&dG%PRnM7Kag#19<(_zJz{C@exu+wOzL;_PvA*IHwwB*NikI891@Z{DF$Hx18ITvEXS z&_boEWg9Lok$yRCTT?TM{!r2c2|m&Fx{nE75;7MW0-42%b_I2+B4&m+X~|>_`@e>h z^FM4OM&03&-hquI?Au)pM}&(?hxy#*DR}?=YO@-ABeukZuE`JKRl65AN#eE^V;KhL z6vZfF`sAMNif!;^3{m-;hUH&sW*6I#v&$}yZ&Di>TW@yAb7?#AKL1l!T zLB(q9L4eG2WbcUGHS$FY2bF)I)eq5?rrLyb-f0dvj`nL}x(2>TkA4P0=@#td)o&V# zr6QTMHr=+$Jrp{m3W0gdDD#(i{W~WT0zL7in50v5ohge5AFaNsW~1L-zrM(irZFQF zu$$;h7IvAw^e`qjc;Pe1*6W{?ZfvH82kS`=HGG~wq|C4^9$r*i*HdfJjGvYnK$z^> zrO11Ci!K|cAYPeqcDPvg=s1oE-_bhr4k?TpWzYvp`Zi$zsyM^^YQI{0t)#`}#r1Ql zP4$2lTfhr+ISe%q1}n>Ji>J}vp0Jt9U7v;AP%;-`NQF`CgPw6`ykoyR&P=Q!;ESKv zz!nhHJ?8qVyI@ek6|3r?!27Nm<=nLBNVV?j!OmTq9&r4$Vc>O)z50#Kcfl!YM*M1Gzk|eqX=p1bNam#`x~{WyTa?cV+F{5 zBU^2xSr@EIYY`1Y_xdvN_DTWC*G=*Kg^=0Ckjb~Fw4!UNkeXCYHE>5ye=2B^00K%S zvCi*)YaVOAAy`o1rA*_FeV9FSO4|WgkMOVAhjNm#W(Yjr1l>ys?d33>lfHMQ{|>t^ zApR9jkSItHcAq#^hY%7WC+60ynaN^Oxw=9t)RGSQzZQc$YzS zKwX}#hR1v>Lhk*DM|VQy*Wf%_hqYGDF4%CT&gNIaBHARhOEVD6Nzr+@M@;~j@!m%G zD>m3;XE2GF&A%uB+rb`_{v$)i6i;a7K5l=DGv{}AeImSx3iuy|G`n7M)0M46A^ho; zZw{nXb<_Kr)k3JnH2YQ4QDQGq9m=9PFjF)?YQGgbuqs=PsroR3+DYJ56*Y8L2yBYa zXvR%dh>QgPB&a^zv7LPSr>B&|xex|Sk^Pj0KgR?|GoV&Fd`2MhrE z>3GMWZVDx`bp8vFS@Nx$&VG(|r@Xay;tTj!DTuSR;1C@ys^i#*JJyfg72~sqiQ33K zD?R}39}>v> zriVtaR-2ofoY}i0zuyP>kZ;{1ebP@ju4DQAv!Wxj4*ay?XL%kc=Qwf|%JS;jXjj+O zK`bNG08RkxB?zGYsUppj>+_=PQ_Dx^?UF3oOC`QaK%7+x%4aZ3rHdFPfXriRaRXKG zVIjnRZDu&d2K$)%ub=sY3rCJZ@#Gq#1-b$n30qFwN*kL&3UJB!(*SB-fCl+dzG28f zNw=oDFLz&9@?prE1+A?uK3IJKh7AoP>}LoX_XqH#Ej2eQT_#GDk;rRl$%3|GQTdHb z6yEkC9RieZFR%`_gtC*bWlB0P&iLvlSg;I^M31mNB{mJ_dU9%#BW0)i~|P% z+-xB!^L}>o$Xu=?xa`b9aJhQtAtaRGjYk@397gvH>KK=$w1Cg?V+LtNw_~M;S0EDD z3uH!C%AihGs=(5m;Ge~)7|2M*zfBCQwmrg} zEg~JYTHZPR>tVX}(rUD#_~|Y`jzByDGzDl3`0HQCmdS(o}$ObZ$H3H%srG);&E9ps@N#s`$mnFWcqf#6qa62Af zTA)-*(w{gn{Ya(N!pjlG2=g5*H>cNmd%&ZPY{7=n4!fKi-FwVthIk7tN&s@|8f{F- ztXuTa$t#iM#e7XhU`O3gpvsWxC8f*|KAIUXCkxIR&x%hb{~B;*GelQ*zkb9tJj$)ADF!F?_vycrT^O3FA#$Tv z_N&mk=pP%Z{Jx!|eY_yWS_T}gAkrN;FnQYv&hZ9+ACC6eWRN~4ve(-O9OR7>Xcqmv z0+1;45vEcPj2HbEfJ6EEG`b=6vf&e$AH z)N(1YFvc#vC+ZopquwuE@`V5qZxNO;*f&Jo>7u`khp79NI&OZ0wh_2D$guvhj*Sn; zDdxJXaof1dm>k;%?c!bBQ`=Bzm`6!J(eDMgLDCL0bkUPBb{ox~9WIQ8f4s00E^GjB zK?PGJPB%X!(W$nSxjfwP2BW_9lJJhdmm|>*t ze3KVa#7?(IrJhUnvXKl)_56iPmO(2!LLy#db|vyJkxdyC=|Bi{YTX|EFs!OJn|p=} z|F~2gDfNixqPx~|HrR7@!n`zZm5Z!6FW$%T6d9?ae}hdEoWr4eiI9+DrcW9<+U_u} z>O#ro<93YkBlw5gq3+{sfj^fhUPpF5Ulp zyhviwMyTZ!{8Eveif=vZ>}SB?>26>l>f$4QEkxpkN9g*1>~OWf>hNdY1D{D%tb{Ue zSUL&Jf)KR6T@3coud48njeH~dP||9~H9my3P?IVd9>`<5bCu;sd2Jah<8bZ<4*qm!dkpEgdI3A;pqn2QsC z)UFDx15ha*!k0#|c-~AWT_TwK|D5ufPg)O1IjAF*$wRf6PwI*xm^koCUv&+t*OZ5W zbikNtNtaN2l@sJaJ$i5cZBf2AUzqYV(Pc-8%7jzQOkOFC8Bm{vHz|>pu{%k$0?a>N^kwRt9%inD~ojf!Fdb)tE)+_8uW<+TbjFJGRC*@{yBaAXW zfJc9@l`Huv1<53#qCZU=BqD_y_lFhG&yzXP-(Y}M6v%Tqrp8LSI)&(YRv3p_Ia$9o zYnMAjVpY))D7m2S^2kiLTbe=VO@Uo=y+kch8T_M%%AUaZ^V^YDg^)f6+BBv9|>;lZK>7Qws@ zypwMi9O?``VF;M*W2tI=9-9_6XEqNW^@hwx)yQI4f6*0a{jShvsrdkp;3xg4xI;G;ppG6Jogw#uRpGr`si=erJGSA2xKxH2ku?7nU-EK&g~U-8gxGs`Vn(#W|=2_1Ir&u#vF0uQ7Hcfbs6=pyML( zb^6OXw2x3!*dSB;zYg*sBo6!Bh{y*!8yoT0uds;0JA*K4G_|gHiRdBJnMHb9?FXxah{NYfvLG$7E3qPudPS?oG=fw0 z%|dU`r$35-WZ@=Dh=fJ$&mG!HQ9JH&^>I_Q?H($hu@*ma`VKu4##wLh1z#{;Y2o(J zRl5H7wfIC~%s4m%Ymj534m96z=NKW9bguO?INZ)_0SJeTzB;yZB1D?}4u_HT= zr6p{(`W9I&m%PO#Ge17Q3Fxi6aZTLu8YZcJeau}23Y|);YzF2jh_tki_ETQ6gK&q1 zo~YdGNl2%jSb508r!@TqbWgJ`ij-sU`m8r(l)rF1n#1Rwd(xQWICeC zqTY#L_?K@q+{b^14ay;uIkub{N=v>|v?LLol9UX@0vs6&>h=VSRNoo@1nJ&g#JmY- zxD?mUdBrip98K(K7NqXZCobN8;vB$6w(-xNg6dNb+j}vebRNBX+`Jo^G~V;{yn3=9 zO=|<^VQeEHYIPMbZo75S!qo~Yz6k?q3Hct*k=r;R$@_Bs4ZCQfkB&fuUHH*+W%>tAS=BH za=IqdE0c#OHmo)sIBpRHKvRTFJj{|(A&ZuTdb}hF+%ZVBX8uj3X7X4SIvItfr@d<1s3fi_kCg>+)B~RO zM`NgE?(%m3hpaeox~985#lzl-Y$2RA8hXf|d(|CDCWY`ane{WKIH`I++~smE$V(EJ z8&;+N*5}(&U`l0c5KQR~a`E%zTiVoPY5H_ID;a-(6f}F65Q?PFN(?DpCz-XK?e*Sg zh;rApKle98jf+pp3{sdZ6^Dd%;qMF%>GERZB=te#3HfRv1UD_=15;}?M0QU<5NIjtKTjZQD!^E* z;W1ccBZ}o?kc0UsxS7rMI@sLY{r8@@k(uxS$vyi(eh!2(1M=5YTXf^wQYOm$9E*Zp z%ri)$iJ>c~*3SQLL{kjMU)qpsW5m`I#Q6MZ!6qlQ`IVdSbUcec@D@nnaDJNt=B@m@ za%PXRXC<()5nB?qA(4wyo*#S*1HT9@F4P;W^>b!Wqr0ELm*7oFlgML54`%}2SUQIzXsa-{L zuE2MDQ1I1GzRNN1w+;O-H|icasgcX9?HzMDl8Ga zu}J^&-Z-HGVoz!dv)MJx04B_p-QjFDJ&F}1DQVf^YB668%e4L^UX_Q-Rt%agrr!HY zxZ?jq)muj8(R59tAvgqghYNRicXxM};O=1p!QI{6-QC^Y2?TcyPCoAEUF)oM{>{Ih zp6Tx0dso#i%&f5OE#5rc7X16~B?HY^+5I-MB8E6y?4s2L(j9b=Xwq&u_eU zo!e0`Fv3=kr9$IF$H&VH3UejpbfE8Z*x*PDrgndbBDa~qZ8KP?;*VcXHOv?xU&dF8t=3g2oDwU08yTeGM@SLt^p<&%Zv z>m@1qyx~^2$WRFI+25KZgl9h02dJ~ZF4%~b5~C)+*xJ=g zCBTLyATb7U=TJgQCN4v&xOd@7nNm+4NvAwbAmqPMEI5tV>`Ugo?i@j#cI6T1Br2lC z5|a1J;oO&x2)L&>WzCcT0Usezm(u)DU)|e|jc8Q=ppaq7%1R<62zkMebA=~~Jd#wD z<`*v;VHl&SL9DhV3N^{jF8^VY<|^)Vy5$FR^x+$&R6ncLX5XV{tP#5`wo^Lx9~qL! z(~PMObzQBiDaH1QkcnMyU)l{r=AP)bu6%aFur0$Zukgm!)4kRKlE4W}PcD*CnO~9t zQ!4|8*-at>N}4)Lw0)?ygAi&y5_L&m1Q;P z%h2X$E|C91yKMjitcZdB-B7PSFBd#LZVPxGCQZbiGW1J&@c;bk^xLUHh@0yJ9eiD6 zuwyDF!!W*%Ltd;M8*WBVqlCDuiLim`yKs19-I-?^ zj;l~<;&S?7*B_x5G9{I1JU_QwIVk5;HZkcV&jmMB=+C4v&_Yn8d!@`SqTM+fVmamQ z%c~bM?~UONqO@s>1b_p{AxCh7@`Rgi-4J-6h|%WK*EVrd%Y>ip^a7oYLcOb>M7A?f zz6PQP0*x4tfJr)4M<^7CRop7A0Q=e2;CLX_T&&sYD~f7pBrcU9_duj)eaaVBeO3_| zYVBgOGO9-4O5Vz~-1d~sOolf6+gF#T;GXBX5RdZvr(E4F-ZA8S z*Mj>zU*=!=!GG6C%{yN$zT8@&eI!@rb2vVA)%kj6hPylP)a&s1$0|Fgf(dzrjl&zg z;rXFIvYD=#ql&dF$hmz<5QH;Xm0~S6kMkGt@?gUQ?im&uDvTnmwIQhP+oma%bdj(J zBr@fmqw@lsRgJ}z478qKf)+foqR2azv)`nplgEDEh@o{oxWmDO~D zm!-o-trPssX;(=4t;Jmv|J1gK!7Q8n2_RBNv&&D>@kfm%QRsx_M4+syHo~3N@K~3NVbb* ze88@rR}PNrqpWgyqS{M6nod=Fd-t(0(@jLr^B;^}CVYz4o3N)Ra`ms4%xK#g(g$Z3IS%oRbQc7=u?8UkgmL>4kk87f z!^mw#b5~L1EPiCe_B%3Sd^krD9HX<8GT0^wH*b769oEw3O>vH_ao@kj_337)(XoB{ z7FB13qQHt3%YSgN3hQ@88!XMj^S>*&k}V zdr#dNh=e5<-bV$J&y|$$sPhH4&VV~$%qkXQ_4suW47F0N6jq|WMo&;+n}qn&Sv7TC zWWKYOOK4n2$UXf_Tx6s;xsXb9Ajkc;Y^@SvMk*smO@iukVxz5kVu4NZi~Vxqc0=O4 zF4yZW=43aq(Wg^ZqV5K~>+2z&)w0PfnTbaVs>ulwVjzrY=8<_fkAvha0#Xt%ZJ5%k zyc`}U&Niq78NyJXW^SWalNB7D$+MUobN>xfs@wpK)s<-*r)(9i5k2N=qfEQpYs*3o143Se!usDMRCTp zZN>~%gbYBgOiO(`p2F_hx$p(PJR1U0DLI>AcYdu{9wWT9_Y1F%EvUaT{kfTVdU`X- zH1i!PctJs^crgR|2bDlHoefpm}m2(nT=ByNU zJ(=0gOJEksoAp&e*roM`>`*SH5$|@65*JUCccd0>aZ7(~-k*5NSBhz*jk40*A3|_= zt)mx?IX6NkLiS25%y3*`5G33x*lQc3D2xsC`DY`T4~?yCfYv+U1)?BR#%-(_HScQ( zGyA4QJ2kAL_L}}+$8rFOVJz$^)je7=X5Zp43y2TvMxZy2q13&H5D#Ohy#Iqw8yCZm zDzrTdWBa8RBOk(*y~dfKE)q;1dNSi}+`bTKlOO_7+5CIlygW52Ch7-sAd6WGQwseL z;s?roDCS?)L<#0XuJmQv)?eSV?yZiLYn{cv=z>dh`eg_5%XYQ`?u1D$;Sk_S1JV3n zRi>5WcxPf}qQnx{KypKVogZ1DeeS>SVmecgW=Z2l-%voDS9Va}jNHMK6l2x8Ls%$| z5o72M;bD$vh5Nf4UYLTiPe#P%y;TbRlNTvn(j>!L3s|oYj{G7s?T5cGFJ9!DTDOpd z`kbV78m1*Z-BWT4#nHW`8 z#>3<)`%AE^nO`GMVjJ$RiUQ*~I4;vsMikZf<)?HO@{^a2 zVxdy->ImNBp5IP;RCtLA(fg)9{fqB+!|V3McEw!=8taq3q@X!Z-#@^JfIZMBCn|@l}Qm<^)jhc`?dQfk(mrrsdz?wO%*<9K6fq{wuRhjXZChO9`_Op zpfx80T$_zP4tTKN!T4@6k>J5mL8Gx@^FgrTiROG|CP8Wkj~WEox%K!~FeZm+_MMMi zU@HcBBRiVNec4r#fJzSghv?>XB4>%Dn1&(gp=$R?&&2?lb)5l0^{>7X&}#pkrEy0` zmU!?$ih$fJMJTqiYP zMCLU|y)A?oL08`5CQwIn9;!-@bgNG-?CJ#Qul8Rg>tpvhvA6fbhewYChhloQSM{85978@42ZpBpioi58YdZDsq!eO5W6iCcKG#4s|e zMclld_|b>dii@g1Du!dB7QcYCCWQi9-U6m*Xr35)_iJ z5J~D}tT3rOG3bp+#pMBCamm`vSva)fHE1U2I%JAF;Fe{Y&PYs1i$)YUgC&C^RHT&8 zj{RKC8W))XW*kW@d6t42xtRc9p1#B-Ac`g|F!?H~X*|jgtcphl+a7VRiXM(eLJ1Sn z?%U=rYH0OQlWZ!pLjP;c>$O}!D4FhPg=Hu)sKloGDI7Xy0a4ISCh;e2C2DZ9zYHlF5w4BRMp9-G9^VsuoYJ%%?Et;PW$k{58f-2|?Z zkWlNpR$d_qd~-vy>Fe~Wdi#g^KL^^4!&UOSTyw- zM{rE_KJTm2jv6zEbf($yM3tqmua~R zFth!7zt#K?NRcCDaCCIPHJ+Id&I_ZONXI??>-|-T%?cIkP1ZrEtmfh7Kl`O#bb%?p ze<$l~8;+{p9Eh=9TZ0z;<98wY zH-1vS`rKR}v2dsy8iH;Jb6?ja9~}=o!7m*k(KV^G)DT?(@-1UfkbWb5s7ErGreZNi zN@dp4&ZyRgeu#8kp6{@Vz}fT9ia$uYff(Q~4}sC4m2)TkFHyR6-zN~|!E%;5Ixru` zO*n|S!9cvCcjQw#^F0={s0m2-SQ!s_x=rL1!K3#=Cbg^iRyHR*n0D(T3HnxSoTNT1 z>{U)TA<*|gF6XM%JV9djG;+|OMrYk2*N@&{7Ai4(^Cg<`t#v-M#piSa;-KPZwPjRg zPe?OM8JIWp-m0rF2?_oWUM$IEQmtuG876R-qInYdjFdomS)}aa7`bn{G5kxDK z52jk;$QxllLlPFxl8^^YbDn=Y6#R}dhx@h#VW#2&D2qmRnAjl;y2UY31*5pGs_WHQ zmoo{OE&Wcud2a!mB!=bh$_dyrXTge6yq6Ys2u9AlzdJJ2)sS)%1Klw&G`ptTl!yc!=lf zS&7yZmFB8=hq0+cv1hL`@xu*dpmQRros5Fjh^kx{G1_A=NTu?-IoBIk+(iI(RFhOv zG;|c-9Z!9dEm2d0l)tL!FkXP>=Cd6Bc4Ab4Btdq<Y;M9ICb2a`Whd~HS9_ppDA0;fFyP9Fi0vYwyK7Kv-N;2LI1#^;ypTI>q>x8)WjCdvW>MFL1wxH4LepZX|mrOeYRJAkoyoUHZU(}0Pb@#niv2*d*nYu-bE zQNCc@(80l37?p119o7Ft%1w@OYinzr;(JdRoaALG7aMVznZAnxKYDfylQUyu!8zg) zuAvm})?Vb@>}(9SUyiNj*?0)jPY?0{JX~C43=GJS0jGV4isi2j-e7>}bap~r#4+*5EkYyL{$$#I1akNbitX!-`9Tq+pz zB5WQsEm?JU{oETlN=_4FHO`2qHJAzY7Y3DZ>UbI_`v|YlC$zAUPK0Z>)}|-?78?x( zE}bM~;hqJnHLPu4^n^-{!L2do!Hre3G3jzgeg{v-aiZZMMH|qrV+eo}jP`YLWq{W~ zwS&{2p$|D^!kfxJtaosvUcWQa{}ML^)m8WE6x4-6;a!b5ZYod8uS>Ha)m}X#?P^&KHVlG5xnnTGO>zJ;)naRVKLP@&0wYTDBNI|F6jEJeNfG?q zN4V$$IaE21*)64P4y1i4Oh(8xCh9yj$sW%=C*VF)X?V}%SvJ`Tjgi`nO%~yJ zz*BqX$Si)sy>(i%gP*a24}*|y!S?o!EIxn7su~ zWPQ5+N~_06qobg~Y&AHFoK1wW-gAEd0$bS zuOU!t0JCpyr?kQf{}WfD(VeGb9vdl^GS^ww&ME&h&4*L#D{X^Wa~1exp}|esSK1lp zt%kg0wrO;amFIk!f(jm01d_@E=T}z`45d1!v%ARfC9tfJ8S7>CV5QA_`GP ztzBgNMYgfdNNxoxHTnSDjDPArzGErR6KQ})sR@FTi0)&?m-jI3#N2LcO;>O4TX%1*imt)qg5b zzQb1Rl*X>*H>&{#^UOPJNt(SiRbdt46or5m_o+fUx$p6I084q&y>nR-57p< zdeZE{0aF&Iy%f{3zFTct;%@|@*(gO%mql))e%ElW+XB!jbjjB>J&_VfKJX>n@YidT zuh-VbHf8`X1Ph^>r!}te^l=d0RYoL3Ps&g@(5D+A8^^qd`Yr0ciV`xeV~@8pbp@(M z;d0fs2f8AeZWy(Q)t<<`@Ic?Z(t97Q^Rof7n}=RFBEa@~STl6wf<4 zgNcQB0yaXuex6x7H_bypq&3y }lSPaYZ?+6!bQu3us4yUy(v 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 D!XM!D delta 60805 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 From a9c2c093382aa838b28ccfd2c336324bec85959f Mon Sep 17 00:00:00 2001 From: xieqiyu Date: Thu, 18 Nov 2021 11:41:17 +0800 Subject: [PATCH 29/42] * Optimize the code. --- module/execution/model.php | 32 ++++++++++++++++++++++++-------- module/productplan/model.php | 2 +- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/module/execution/model.php b/module/execution/model.php index 3c2f11638b..7f9a361063 100644 --- a/module/execution/model.php +++ b/module/execution/model.php @@ -3654,16 +3654,32 @@ class executionModel extends model public function getPlans($products) { $this->loadModel('productplan'); - $productPlans = array(); - foreach($products as $productID => $product) + + $branchIDList = array(); + foreach($products as $product) { - $planInfo = $this->productplan->getPairs($product->id, implode(',', $product->branches)); - foreach($planInfo as $planID => $plan) - { - if(empty($planID)) continue; - $productPlans[$productID][$planID] = $plan; - } + foreach($product->branches as $branchID) $branchIDList[$branchID] = $branchID; } + + $plans = $this->dao->select('id,title,product,parent,begin,end')->from(TABLE_PRODUCTPLAN) + ->where('product')->in(array_keys($products)) + ->andWhere('deleted')->eq(0) + ->andWhere('branch')->in($branchIDList)->fi() + ->orderBy('begin desc') + ->fetchAll('id'); + + $plans = $this->productplan->reorder4Children($plans); + $productPlans = array(); + $parentTitle = array(); + foreach($plans as $plan) + { + if($plan->parent == '-1') $parentTitle[$plan->id] = $plan->title; + if($plan->parent > 0 and isset($parentTitle[$plan->parent])) $plan->title = $parentTitle[$plan->parent] . ' /' . $plan->title; + + $productPlans[$plan->product][$plan->id] = $plan->title . " [{$plan->begin} ~ {$plan->end}]"; + if($plan->begin == '2030-01-01' and $plan->end == '2030-01-01') $productPlans[$plan->product][$plan->id] = $plan->title . ' ' . $this->lang->productplan->future; + } + return $productPlans; } diff --git a/module/productplan/model.php b/module/productplan/model.php index b7acbb6005..893a17f060 100644 --- a/module/productplan/model.php +++ b/module/productplan/model.php @@ -184,7 +184,7 @@ class productplanModel extends model $plans = $this->dao->select('id,title,parent,begin,end')->from(TABLE_PRODUCTPLAN) ->where('product')->in($product) ->andWhere('deleted')->eq(0) - ->beginIF($branch !== '')->andWhere('branch')->in("$branch")->fi() + ->beginIF($branch !== '')->andWhere('branch')->eq($branch)->fi() ->beginIF($expired == 'unexpired')->andWhere('end')->ge($date)->fi() ->beginIF($skipParent)->andWhere('parent')->ne(-1)->fi() ->orderBy('begin desc') From 8c1cc5c875269ce2085d35840602a6b353494c35 Mon Sep 17 00:00:00 2001 From: zhujinyong Date: Thu, 18 Nov 2021 13:35:32 +0800 Subject: [PATCH 30/42] * Update purifier library. --- lib/purifier/HTMLPurifier.auto.php | 11 + lib/purifier/HTMLPurifier.autoload-legacy.php | 15 + lib/purifier/HTMLPurifier.autoload.php | 51 +- lib/purifier/HTMLPurifier.composer.php | 8 +- lib/purifier/HTMLPurifier.func.php | 50 +- lib/purifier/HTMLPurifier.includes.php | 468 +- lib/purifier/HTMLPurifier.kses.php | 60 +- lib/purifier/HTMLPurifier.path.php | 22 +- lib/purifier/HTMLPurifier.php | 589 +- lib/purifier/HTMLPurifier.safe-includes.php | 456 +- lib/purifier/HTMLPurifier/Arborize.php | 142 +- lib/purifier/HTMLPurifier/AttrCollections.php | 296 +- lib/purifier/HTMLPurifier/AttrDef.php | 288 +- lib/purifier/HTMLPurifier/AttrDef/CSS.php | 272 +- .../HTMLPurifier/AttrDef/CSS/AlphaValue.php | 68 +- .../HTMLPurifier/AttrDef/CSS/Background.php | 222 +- .../AttrDef/CSS/BackgroundPosition.php | 314 +- .../HTMLPurifier/AttrDef/CSS/Border.php | 112 +- .../HTMLPurifier/AttrDef/CSS/Color.php | 322 +- .../HTMLPurifier/AttrDef/CSS/Composite.php | 96 +- .../AttrDef/CSS/DenyElementDecorator.php | 88 +- .../HTMLPurifier/AttrDef/CSS/Filter.php | 154 +- .../HTMLPurifier/AttrDef/CSS/Font.php | 352 +- .../HTMLPurifier/AttrDef/CSS/FontFamily.php | 438 +- .../HTMLPurifier/AttrDef/CSS/Ident.php | 64 +- .../AttrDef/CSS/ImportantDecorator.php | 112 +- .../HTMLPurifier/AttrDef/CSS/Length.php | 154 +- .../HTMLPurifier/AttrDef/CSS/ListStyle.php | 224 +- .../HTMLPurifier/AttrDef/CSS/Multiple.php | 142 +- .../HTMLPurifier/AttrDef/CSS/Number.php | 174 +- .../HTMLPurifier/AttrDef/CSS/Percentage.php | 108 +- .../AttrDef/CSS/TextDecoration.php | 92 +- lib/purifier/HTMLPurifier/AttrDef/CSS/URI.php | 154 +- lib/purifier/HTMLPurifier/AttrDef/Clone.php | 88 +- lib/purifier/HTMLPurifier/AttrDef/Enum.php | 146 +- .../HTMLPurifier/AttrDef/HTML/Bool.php | 96 +- .../HTMLPurifier/AttrDef/HTML/Class.php | 96 +- .../HTMLPurifier/AttrDef/HTML/Color.php | 102 +- .../HTMLPurifier/AttrDef/HTML/FrameTarget.php | 76 +- lib/purifier/HTMLPurifier/AttrDef/HTML/ID.php | 226 +- .../HTMLPurifier/AttrDef/HTML/Length.php | 112 +- .../HTMLPurifier/AttrDef/HTML/LinkTypes.php | 144 +- .../HTMLPurifier/AttrDef/HTML/MultiLength.php | 120 +- .../HTMLPurifier/AttrDef/HTML/Nmtokens.php | 140 +- .../HTMLPurifier/AttrDef/HTML/Pixels.php | 152 +- lib/purifier/HTMLPurifier/AttrDef/Integer.php | 182 +- lib/purifier/HTMLPurifier/AttrDef/Lang.php | 172 +- lib/purifier/HTMLPurifier/AttrDef/Switch.php | 106 +- lib/purifier/HTMLPurifier/AttrDef/Text.php | 42 +- lib/purifier/HTMLPurifier/AttrDef/URI.php | 222 +- .../HTMLPurifier/AttrDef/URI/Email.php | 40 +- .../AttrDef/URI/Email/SimpleCheck.php | 58 +- .../HTMLPurifier/AttrDef/URI/Host.php | 280 +- .../HTMLPurifier/AttrDef/URI/IPv4.php | 90 +- .../HTMLPurifier/AttrDef/URI/IPv6.php | 178 +- lib/purifier/HTMLPurifier/AttrTransform.php | 120 +- .../HTMLPurifier/AttrTransform/Background.php | 56 +- .../HTMLPurifier/AttrTransform/BdoDir.php | 54 +- .../HTMLPurifier/AttrTransform/BgColor.php | 56 +- .../HTMLPurifier/AttrTransform/BoolToCSS.php | 94 +- .../HTMLPurifier/AttrTransform/Border.php | 52 +- .../HTMLPurifier/AttrTransform/EnumToCSS.php | 136 +- .../AttrTransform/ImgRequired.php | 94 +- .../HTMLPurifier/AttrTransform/ImgSpace.php | 122 +- .../HTMLPurifier/AttrTransform/Input.php | 112 +- .../HTMLPurifier/AttrTransform/Lang.php | 62 +- .../HTMLPurifier/AttrTransform/Length.php | 90 +- .../HTMLPurifier/AttrTransform/Name.php | 66 +- .../HTMLPurifier/AttrTransform/NameSync.php | 82 +- .../HTMLPurifier/AttrTransform/Nofollow.php | 104 +- .../HTMLPurifier/AttrTransform/SafeEmbed.php | 50 +- .../HTMLPurifier/AttrTransform/SafeObject.php | 56 +- .../HTMLPurifier/AttrTransform/SafeParam.php | 158 +- .../AttrTransform/ScriptRequired.php | 46 +- .../AttrTransform/TargetBlank.php | 90 +- .../AttrTransform/TargetNoopener.php | 74 +- .../AttrTransform/TargetNoreferrer.php | 74 +- .../HTMLPurifier/AttrTransform/Textarea.php | 54 +- lib/purifier/HTMLPurifier/AttrTypes.php | 192 +- lib/purifier/HTMLPurifier/AttrValidator.php | 356 +- lib/purifier/HTMLPurifier/Bootstrap.php | 248 +- lib/purifier/HTMLPurifier/CSSDefinition.php | 1024 +- lib/purifier/HTMLPurifier/ChildDef.php | 104 +- .../HTMLPurifier/ChildDef/Chameleon.php | 134 +- lib/purifier/HTMLPurifier/ChildDef/Custom.php | 204 +- lib/purifier/HTMLPurifier/ChildDef/Empty.php | 76 +- lib/purifier/HTMLPurifier/ChildDef/List.php | 184 +- .../HTMLPurifier/ChildDef/Optional.php | 90 +- .../HTMLPurifier/ChildDef/Required.php | 236 +- .../ChildDef/StrictBlockquote.php | 220 +- lib/purifier/HTMLPurifier/ChildDef/Table.php | 448 +- lib/purifier/HTMLPurifier/Config.php | 1840 ++-- lib/purifier/HTMLPurifier/ConfigSchema.php | 352 +- .../ConfigSchema/Builder/ConfigSchema.php | 96 +- .../HTMLPurifier/ConfigSchema/Builder/Xml.php | 288 +- .../HTMLPurifier/ConfigSchema/Exception.php | 22 +- .../HTMLPurifier/ConfigSchema/Interchange.php | 94 +- .../ConfigSchema/Interchange/Directive.php | 178 +- .../ConfigSchema/Interchange/Id.php | 116 +- .../ConfigSchema/InterchangeBuilder.php | 452 +- .../HTMLPurifier/ConfigSchema/Validator.php | 496 +- .../ConfigSchema/ValidatorAtom.php | 260 +- .../HTMLPurifier/ConfigSchema/schema.ser | Bin 15923 -> 24190 bytes .../schema/Attr.AllowedClasses.txt | 16 +- .../schema/Attr.AllowedFrameTargets.txt | 24 +- .../ConfigSchema/schema/Attr.AllowedRel.txt | 18 +- .../ConfigSchema/schema/Attr.AllowedRev.txt | 18 +- .../schema/Attr.ClassUseCDATA.txt | 38 +- .../schema/Attr.DefaultImageAlt.txt | 22 +- .../schema/Attr.DefaultInvalidImage.txt | 18 +- .../schema/Attr.DefaultInvalidImageAlt.txt | 16 +- .../schema/Attr.DefaultTextDir.txt | 20 +- .../ConfigSchema/schema/Attr.EnableID.txt | 32 +- .../schema/Attr.ForbiddenClasses.txt | 16 +- .../ConfigSchema/schema/Attr.ID.HTML5.txt | 20 +- .../ConfigSchema/schema/Attr.IDBlacklist.txt | 10 +- .../schema/Attr.IDBlacklistRegexp.txt | 18 +- .../ConfigSchema/schema/Attr.IDPrefix.txt | 24 +- .../schema/Attr.IDPrefixLocal.txt | 28 +- .../schema/AutoFormat.AutoParagraph.txt | 62 +- .../ConfigSchema/schema/AutoFormat.Custom.txt | 24 +- .../schema/AutoFormat.DisplayLinkURI.txt | 22 +- .../schema/AutoFormat.Linkify.txt | 24 +- .../AutoFormat.PurifierLinkify.DocURL.txt | 24 +- .../schema/AutoFormat.PurifierLinkify.txt | 24 +- .../AutoFormat.RemoveEmpty.Predicate.txt | 28 +- ...rmat.RemoveEmpty.RemoveNbsp.Exceptions.txt | 22 +- .../AutoFormat.RemoveEmpty.RemoveNbsp.txt | 30 +- .../schema/AutoFormat.RemoveEmpty.txt | 92 +- ...utoFormat.RemoveSpansWithoutAttributes.txt | 22 +- .../schema/CSS.AllowDuplicates.txt | 22 +- .../schema/CSS.AllowImportant.txt | 16 +- .../ConfigSchema/schema/CSS.AllowTricky.txt | 22 +- .../ConfigSchema/schema/CSS.AllowedFonts.txt | 24 +- .../schema/CSS.AllowedProperties.txt | 36 +- .../ConfigSchema/schema/CSS.DefinitionRev.txt | 22 +- .../schema/CSS.ForbiddenProperties.txt | 26 +- .../ConfigSchema/schema/CSS.MaxImgLength.txt | 32 +- .../ConfigSchema/schema/CSS.Proprietary.txt | 20 +- .../ConfigSchema/schema/CSS.Trusted.txt | 18 +- .../schema/Cache.DefinitionImpl.txt | 28 +- .../schema/Cache.SerializerPath.txt | 26 +- .../schema/Cache.SerializerPermissions.txt | 32 +- .../schema/Core.AggressivelyFixLt.txt | 36 +- .../schema/Core.AggressivelyRemoveScript.txt | 32 +- .../schema/Core.AllowHostnameUnderscore.txt | 32 +- .../schema/Core.AllowParseManyTags.txt | 12 + .../schema/Core.CollectErrors.txt | 24 +- .../schema/Core.ColorKeywords.txt | 189 +- .../schema/Core.ConvertDocumentToFragment.txt | 28 +- .../Core.DirectLexLineNumberSyncInterval.txt | 34 +- .../schema/Core.DisableExcludes.txt | 28 +- .../ConfigSchema/schema/Core.EnableIDNA.txt | 18 +- .../ConfigSchema/schema/Core.Encoding.txt | 30 +- .../schema/Core.EscapeInvalidChildren.txt | 24 +- .../schema/Core.EscapeInvalidTags.txt | 14 +- .../schema/Core.EscapeNonASCIICharacters.txt | 26 +- .../schema/Core.HiddenElements.txt | 38 +- .../ConfigSchema/schema/Core.Language.txt | 20 +- .../schema/Core.LegacyEntityDecoder.txt | 72 +- .../ConfigSchema/schema/Core.LexerImpl.txt | 68 +- .../schema/Core.MaintainLineNumbers.txt | 32 +- .../schema/Core.NormalizeNewlines.txt | 22 +- .../schema/Core.RemoveInvalidImg.txt | 24 +- .../Core.RemoveProcessingInstructions.txt | 22 +- .../schema/Core.RemoveScriptContents.txt | 24 +- .../ConfigSchema/schema/Filter.Custom.txt | 22 +- .../Filter.ExtractStyleBlocks.Escaping.txt | 28 +- .../Filter.ExtractStyleBlocks.Scope.txt | 58 +- .../Filter.ExtractStyleBlocks.TidyImpl.txt | 32 +- .../schema/Filter.ExtractStyleBlocks.txt | 148 +- .../ConfigSchema/schema/Filter.YouTube.txt | 32 +- .../ConfigSchema/schema/HTML.Allowed.txt | 50 +- .../schema/HTML.AllowedAttributes.txt | 38 +- .../schema/HTML.AllowedComments.txt | 20 +- .../schema/HTML.AllowedCommentsRegexp.txt | 30 +- .../schema/HTML.AllowedElements.txt | 46 +- .../schema/HTML.AllowedModules.txt | 40 +- .../schema/HTML.Attr.Name.UseCDATA.txt | 22 +- .../ConfigSchema/schema/HTML.BlockWrapper.txt | 36 +- .../ConfigSchema/schema/HTML.CoreModules.txt | 46 +- .../schema/HTML.CustomDoctype.txt | 18 +- .../ConfigSchema/schema/HTML.DefinitionID.txt | 66 +- .../schema/HTML.DefinitionRev.txt | 32 +- .../ConfigSchema/schema/HTML.Doctype.txt | 22 +- .../schema/HTML.FlashAllowFullScreen.txt | 22 +- .../schema/HTML.ForbiddenAttributes.txt | 42 +- .../schema/HTML.ForbiddenElements.txt | 40 +- .../ConfigSchema/schema/HTML.Forms.txt | 11 + .../ConfigSchema/schema/HTML.MaxImgLength.txt | 28 +- .../ConfigSchema/schema/HTML.Nofollow.txt | 14 +- .../ConfigSchema/schema/HTML.Parent.txt | 24 +- .../ConfigSchema/schema/HTML.Proprietary.txt | 24 +- .../ConfigSchema/schema/HTML.SafeEmbed.txt | 26 +- .../ConfigSchema/schema/HTML.SafeIframe.txt | 26 +- .../ConfigSchema/schema/HTML.SafeObject.txt | 26 +- .../schema/HTML.SafeScripting.txt | 20 +- .../ConfigSchema/schema/HTML.Strict.txt | 18 +- .../ConfigSchema/schema/HTML.TargetBlank.txt | 16 +- .../schema/HTML.TargetNoopener.txt | 20 +- .../schema/HTML.TargetNoreferrer.txt | 18 +- .../ConfigSchema/schema/HTML.TidyAdd.txt | 16 +- .../ConfigSchema/schema/HTML.TidyLevel.txt | 48 +- .../ConfigSchema/schema/HTML.TidyRemove.txt | 16 +- .../ConfigSchema/schema/HTML.Trusted.txt | 18 +- .../ConfigSchema/schema/HTML.XHTML.txt | 22 +- .../schema/Output.CommentScriptContents.txt | 20 +- .../schema/Output.FixInnerHTML.txt | 30 +- .../schema/Output.FlashCompat.txt | 22 +- .../ConfigSchema/schema/Output.Newline.txt | 26 +- .../ConfigSchema/schema/Output.SortAttr.txt | 28 +- .../ConfigSchema/schema/Output.TidyFormat.txt | 50 +- .../ConfigSchema/schema/Test.ForceNoIconv.txt | 14 +- .../schema/URI.AllowedSchemes.txt | 36 +- .../ConfigSchema/schema/URI.Base.txt | 34 +- .../ConfigSchema/schema/URI.DefaultScheme.txt | 30 +- .../ConfigSchema/schema/URI.DefinitionID.txt | 22 +- .../ConfigSchema/schema/URI.DefinitionRev.txt | 22 +- .../ConfigSchema/schema/URI.Disable.txt | 28 +- .../schema/URI.DisableExternal.txt | 22 +- .../schema/URI.DisableExternalResources.txt | 26 +- .../schema/URI.DisableResources.txt | 30 +- .../ConfigSchema/schema/URI.Host.txt | 38 +- .../ConfigSchema/schema/URI.HostBlacklist.txt | 18 +- .../ConfigSchema/schema/URI.MakeAbsolute.txt | 26 +- .../ConfigSchema/schema/URI.Munge.txt | 166 +- .../schema/URI.MungeResources.txt | 34 +- .../schema/URI.MungeSecretKey.txt | 60 +- .../schema/URI.OverrideAllowedSchemes.txt | 18 +- .../schema/URI.SafeIframeRegexp.txt | 44 +- .../HTMLPurifier/ConfigSchema/schema/info.ini | 6 +- lib/purifier/HTMLPurifier/ContentSets.php | 340 +- lib/purifier/HTMLPurifier/Context.php | 190 +- lib/purifier/HTMLPurifier/Definition.php | 110 +- lib/purifier/HTMLPurifier/DefinitionCache.php | 258 +- .../DefinitionCache/Decorator.php | 224 +- .../DefinitionCache/Decorator/Cleanup.php | 156 +- .../DefinitionCache/Decorator/Memory.php | 170 +- .../DefinitionCache/Decorator/Template.php.in | 164 +- .../HTMLPurifier/DefinitionCache/Null.php | 152 +- .../DefinitionCache/Serializer.php | 617 +- .../DefinitionCache/Serializer/README | 6 +- .../HTMLPurifier/DefinitionCacheFactory.php | 212 +- lib/purifier/HTMLPurifier/Doctype.php | 146 +- lib/purifier/HTMLPurifier/DoctypeRegistry.php | 284 +- lib/purifier/HTMLPurifier/ElementDef.php | 432 +- lib/purifier/HTMLPurifier/Encoder.php | 1234 +-- lib/purifier/HTMLPurifier/EntityLookup.php | 96 +- lib/purifier/HTMLPurifier/EntityParser.php | 570 +- lib/purifier/HTMLPurifier/ErrorCollector.php | 488 +- lib/purifier/HTMLPurifier/ErrorStruct.php | 148 +- lib/purifier/HTMLPurifier/Exception.php | 24 +- lib/purifier/HTMLPurifier/Filter.php | 112 +- .../Filter/ExtractStyleBlocks.php | 682 +- lib/purifier/HTMLPurifier/Filter/YouTube.php | 130 +- lib/purifier/HTMLPurifier/Generator.php | 572 +- lib/purifier/HTMLPurifier/HTMLDefinition.php | 986 +- lib/purifier/HTMLPurifier/HTMLModule.php | 568 +- lib/purifier/HTMLPurifier/HTMLModule/Bdo.php | 88 +- .../HTMLModule/CommonAttributes.php | 62 +- lib/purifier/HTMLPurifier/HTMLModule/Edit.php | 110 +- .../HTMLPurifier/HTMLModule/Forms.php | 384 +- .../HTMLPurifier/HTMLModule/Hypertext.php | 80 +- .../HTMLPurifier/HTMLModule/Iframe.php | 102 +- .../HTMLPurifier/HTMLModule/Image.php | 98 +- .../HTMLPurifier/HTMLModule/Legacy.php | 372 +- lib/purifier/HTMLPurifier/HTMLModule/List.php | 102 +- lib/purifier/HTMLPurifier/HTMLModule/Name.php | 52 +- .../HTMLPurifier/HTMLModule/Nofollow.php | 50 +- .../HTMLModule/NonXMLCommonAttributes.php | 40 +- .../HTMLPurifier/HTMLModule/Object.php | 124 +- .../HTMLPurifier/HTMLModule/Presentation.php | 84 +- .../HTMLPurifier/HTMLModule/Proprietary.php | 80 +- lib/purifier/HTMLPurifier/HTMLModule/Ruby.php | 72 +- .../HTMLPurifier/HTMLModule/SafeEmbed.php | 80 +- .../HTMLPurifier/HTMLModule/SafeObject.php | 124 +- .../HTMLPurifier/HTMLModule/SafeScripting.php | 80 +- .../HTMLPurifier/HTMLModule/Scripting.php | 146 +- .../HTMLModule/StyleAttribute.php | 66 +- .../HTMLPurifier/HTMLModule/Tables.php | 150 +- .../HTMLPurifier/HTMLModule/Target.php | 56 +- .../HTMLPurifier/HTMLModule/TargetBlank.php | 48 +- .../HTMLModule/TargetNoopener.php | 42 +- .../HTMLModule/TargetNoreferrer.php | 42 +- lib/purifier/HTMLPurifier/HTMLModule/Text.php | 174 +- lib/purifier/HTMLPurifier/HTMLModule/Tidy.php | 460 +- .../HTMLPurifier/HTMLModule/Tidy/Name.php | 66 +- .../HTMLModule/Tidy/Proprietary.php | 68 +- .../HTMLPurifier/HTMLModule/Tidy/Strict.php | 86 +- .../HTMLModule/Tidy/Transitional.php | 32 +- .../HTMLPurifier/HTMLModule/Tidy/XHTML.php | 52 +- .../HTMLModule/Tidy/XHTMLAndHTML4.php | 359 +- .../HTMLModule/XMLCommonAttributes.php | 40 +- .../HTMLPurifier/HTMLModuleManager.php | 934 +- lib/purifier/HTMLPurifier/IDAccumulator.php | 114 +- lib/purifier/HTMLPurifier/Injector.php | 564 +- .../HTMLPurifier/Injector/AutoParagraph.php | 712 +- .../HTMLPurifier/Injector/DisplayLinkURI.php | 80 +- .../HTMLPurifier/Injector/Linkify.php | 128 +- .../HTMLPurifier/Injector/PurifierLinkify.php | 142 +- .../HTMLPurifier/Injector/RemoveEmpty.php | 224 +- .../Injector/RemoveSpansWithoutAttributes.php | 168 +- .../HTMLPurifier/Injector/SafeObject.php | 248 +- lib/purifier/HTMLPurifier/Language.php | 408 +- .../HTMLPurifier/Language/messages/en.php | 110 +- lib/purifier/HTMLPurifier/LanguageFactory.php | 418 +- lib/purifier/HTMLPurifier/Length.php | 322 +- lib/purifier/HTMLPurifier/Lexer.php | 764 +- lib/purifier/HTMLPurifier/Lexer/DOMLex.php | 629 +- lib/purifier/HTMLPurifier/Lexer/DirectLex.php | 1078 +- lib/purifier/HTMLPurifier/Lexer/PH5P.php | 9576 ++++++++--------- lib/purifier/HTMLPurifier/Node.php | 98 +- lib/purifier/HTMLPurifier/Node/Comment.php | 72 +- lib/purifier/HTMLPurifier/Node/Element.php | 118 +- lib/purifier/HTMLPurifier/Node/Text.php | 108 +- lib/purifier/HTMLPurifier/PercentEncoder.php | 222 +- lib/purifier/HTMLPurifier/Printer.php | 436 +- .../HTMLPurifier/Printer/CSSDefinition.php | 88 +- .../HTMLPurifier/Printer/ConfigForm.css | 20 +- .../HTMLPurifier/Printer/ConfigForm.js | 10 +- .../HTMLPurifier/Printer/ConfigForm.php | 902 +- .../HTMLPurifier/Printer/HTMLDefinition.php | 648 +- lib/purifier/HTMLPurifier/PropertyList.php | 244 +- .../HTMLPurifier/PropertyListIterator.php | 84 +- lib/purifier/HTMLPurifier/Queue.php | 112 +- lib/purifier/HTMLPurifier/Strategy.php | 52 +- .../HTMLPurifier/Strategy/Composite.php | 60 +- lib/purifier/HTMLPurifier/Strategy/Core.php | 34 +- .../HTMLPurifier/Strategy/FixNesting.php | 362 +- .../HTMLPurifier/Strategy/MakeWellFormed.php | 1318 +-- .../Strategy/RemoveForeignElements.php | 414 +- .../Strategy/ValidateAttributes.php | 90 +- lib/purifier/HTMLPurifier/StringHash.php | 94 +- .../HTMLPurifier/StringHashParser.php | 272 +- lib/purifier/HTMLPurifier/TagTransform.php | 74 +- .../HTMLPurifier/TagTransform/Font.php | 228 +- .../HTMLPurifier/TagTransform/Simple.php | 88 +- lib/purifier/HTMLPurifier/Token.php | 200 +- lib/purifier/HTMLPurifier/Token/Comment.php | 76 +- lib/purifier/HTMLPurifier/Token/Empty.php | 30 +- lib/purifier/HTMLPurifier/Token/End.php | 48 +- lib/purifier/HTMLPurifier/Token/Start.php | 20 +- lib/purifier/HTMLPurifier/Token/Tag.php | 136 +- lib/purifier/HTMLPurifier/Token/Text.php | 106 +- lib/purifier/HTMLPurifier/TokenFactory.php | 236 +- lib/purifier/HTMLPurifier/URI.php | 632 +- lib/purifier/HTMLPurifier/URIDefinition.php | 224 +- lib/purifier/HTMLPurifier/URIFilter.php | 148 +- .../URIFilter/DisableExternal.php | 108 +- .../URIFilter/DisableExternalResources.php | 50 +- .../URIFilter/DisableResources.php | 44 +- .../HTMLPurifier/URIFilter/HostBlacklist.php | 92 +- .../HTMLPurifier/URIFilter/MakeAbsolute.php | 316 +- lib/purifier/HTMLPurifier/URIFilter/Munge.php | 230 +- .../HTMLPurifier/URIFilter/SafeIframe.php | 136 +- lib/purifier/HTMLPurifier/URIParser.php | 142 +- lib/purifier/HTMLPurifier/URIScheme.php | 204 +- lib/purifier/HTMLPurifier/URIScheme/data.php | 272 +- lib/purifier/HTMLPurifier/URIScheme/file.php | 88 +- lib/purifier/HTMLPurifier/URIScheme/ftp.php | 116 +- lib/purifier/HTMLPurifier/URIScheme/http.php | 72 +- lib/purifier/HTMLPurifier/URIScheme/https.php | 36 +- .../HTMLPurifier/URIScheme/mailto.php | 80 +- lib/purifier/HTMLPurifier/URIScheme/news.php | 70 +- lib/purifier/HTMLPurifier/URIScheme/nntp.php | 64 +- lib/purifier/HTMLPurifier/URIScheme/tel.php | 92 +- .../HTMLPurifier/URISchemeRegistry.php | 162 +- lib/purifier/HTMLPurifier/UnitConverter.php | 614 +- lib/purifier/HTMLPurifier/VarParser.php | 396 +- .../HTMLPurifier/VarParser/Flexible.php | 260 +- .../HTMLPurifier/VarParser/Native.php | 76 +- .../HTMLPurifier/VarParserException.php | 22 +- lib/purifier/HTMLPurifier/Zipper.php | 314 +- 373 files changed, 32415 insertions(+), 32120 deletions(-) create mode 100644 lib/purifier/HTMLPurifier.auto.php create mode 100644 lib/purifier/HTMLPurifier.autoload-legacy.php create mode 100644 lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.AllowParseManyTags.txt create mode 100644 lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Forms.txt diff --git a/lib/purifier/HTMLPurifier.auto.php b/lib/purifier/HTMLPurifier.auto.php new file mode 100644 index 0000000000..1960c399f8 --- /dev/null +++ b/lib/purifier/HTMLPurifier.auto.php @@ -0,0 +1,11 @@ +purify($html, $config); -} - -// vim: et sw=4 sts=4 +purify($html, $config); +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier.includes.php b/lib/purifier/HTMLPurifier.includes.php index 6335e7c861..151e6752df 100644 --- a/lib/purifier/HTMLPurifier.includes.php +++ b/lib/purifier/HTMLPurifier.includes.php @@ -1,234 +1,234 @@ - $attributes) { - $allowed_elements[$element] = true; - foreach ($attributes as $attribute => $x) { - $allowed_attributes["$element.$attribute"] = true; - } - } - $config->set('HTML.AllowedElements', $allowed_elements); - $config->set('HTML.AllowedAttributes', $allowed_attributes); - if ($allowed_protocols !== null) { - $config->set('URI.AllowedSchemes', $allowed_protocols); - } - $purifier = new HTMLPurifier($config); - return $purifier->purify($string); -} - -// vim: et sw=4 sts=4 + $attributes) { + $allowed_elements[$element] = true; + foreach ($attributes as $attribute => $x) { + $allowed_attributes["$element.$attribute"] = true; + } + } + $config->set('HTML.AllowedElements', $allowed_elements); + $config->set('HTML.AllowedAttributes', $allowed_attributes); + if ($allowed_protocols !== null) { + $config->set('URI.AllowedSchemes', $allowed_protocols); + } + $purifier = new HTMLPurifier($config); + return $purifier->purify($string); +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier.path.php b/lib/purifier/HTMLPurifier.path.php index 353492a1cc..39b1b65319 100644 --- a/lib/purifier/HTMLPurifier.path.php +++ b/lib/purifier/HTMLPurifier.path.php @@ -1,11 +1,11 @@ -config = HTMLPurifier_Config::create($config); - $this->strategy = new HTMLPurifier_Strategy_Core(); - } - - /** - * Adds a filter to process the output. First come first serve - * - * @param HTMLPurifier_Filter $filter HTMLPurifier_Filter object - */ - public function addFilter($filter) - { - trigger_error( - 'HTMLPurifier->addFilter() is deprecated, use configuration directives' . - ' in the Filter namespace or Filter.Custom', - E_USER_WARNING - ); - $this->filters[] = $filter; - } - - /** - * Filters an HTML snippet/document to be XSS-free and standards-compliant. - * - * @param string $html String of HTML to purify - * @param HTMLPurifier_Config $config Config object for this operation, - * if omitted, defaults to the config object specified during this - * object's construction. The parameter can also be any type - * that HTMLPurifier_Config::create() supports. - * - * @return string Purified HTML - */ - public function purify($html, $config = null) - { - // :TODO: make the config merge in, instead of replace - $config = $config ? HTMLPurifier_Config::create($config) : $this->config; - - // implementation is partially environment dependant, partially - // configuration dependant - $lexer = HTMLPurifier_Lexer::create($config); - - $context = new HTMLPurifier_Context(); - - // setup HTML generator - $this->generator = new HTMLPurifier_Generator($config, $context); - $context->register('Generator', $this->generator); - - // set up global context variables - if ($config->get('Core.CollectErrors')) { - // may get moved out if other facilities use it - $language_factory = HTMLPurifier_LanguageFactory::instance(); - $language = $language_factory->create($config, $context); - $context->register('Locale', $language); - - $error_collector = new HTMLPurifier_ErrorCollector($context); - $context->register('ErrorCollector', $error_collector); - } - - // setup id_accumulator context, necessary due to the fact that - // AttrValidator can be called from many places - $id_accumulator = HTMLPurifier_IDAccumulator::build($config, $context); - $context->register('IDAccumulator', $id_accumulator); - - $html = HTMLPurifier_Encoder::convertToUTF8($html, $config, $context); - - // setup filters - $filter_flags = $config->getBatch('Filter'); - $custom_filters = $filter_flags['Custom']; - unset($filter_flags['Custom']); - $filters = array(); - foreach ($filter_flags as $filter => $flag) { - if (!$flag) { - continue; - } - if (strpos($filter, '.') !== false) { - continue; - } - $class = "HTMLPurifier_Filter_$filter"; - $filters[] = new $class; - } - foreach ($custom_filters as $filter) { - // maybe "HTMLPurifier_Filter_$filter", but be consistent with AutoFormat - $filters[] = $filter; - } - $filters = array_merge($filters, $this->filters); - // maybe prepare(), but later - - for ($i = 0, $filter_size = count($filters); $i < $filter_size; $i++) { - $html = $filters[$i]->preFilter($html, $config, $context); - } - - // purified HTML - $html = - $this->generator->generateFromTokens( - // list of tokens - $this->strategy->execute( - // list of un-purified tokens - $lexer->tokenizeHTML( - // un-purified HTML - $html, - $config, - $context - ), - $config, - $context - ) - ); - - for ($i = $filter_size - 1; $i >= 0; $i--) { - $html = $filters[$i]->postFilter($html, $config, $context); - } - - $html = HTMLPurifier_Encoder::convertFromUTF8($html, $config, $context); - $this->context =& $context; - return $html; - } - - /** - * Filters an array of HTML snippets - * - * @param string[] $array_of_html Array of html snippets - * @param HTMLPurifier_Config $config Optional config object for this operation. - * See HTMLPurifier::purify() for more details. - * - * @return string[] Array of purified HTML - */ - public function purifyArray($array_of_html, $config = null) - { - $context_array = array(); - foreach ($array_of_html as $key => $html) { - $array_of_html[$key] = $this->purify($html, $config); - $context_array[$key] = $this->context; - } - $this->context = $context_array; - return $array_of_html; - } - - /** - * Singleton for enforcing just one HTML Purifier in your system - * - * @param HTMLPurifier|HTMLPurifier_Config $prototype Optional prototype - * HTMLPurifier instance to overload singleton with, - * or HTMLPurifier_Config instance to configure the - * generated version with. - * - * @return HTMLPurifier - */ - public static function instance($prototype = null) - { - if (!self::$instance || $prototype) { - if ($prototype instanceof HTMLPurifier) { - self::$instance = $prototype; - } elseif ($prototype) { - self::$instance = new HTMLPurifier($prototype); - } else { - self::$instance = new HTMLPurifier(); - } - } - return self::$instance; - } - - /** - * Singleton for enforcing just one HTML Purifier in your system - * - * @param HTMLPurifier|HTMLPurifier_Config $prototype Optional prototype - * HTMLPurifier instance to overload singleton with, - * or HTMLPurifier_Config instance to configure the - * generated version with. - * - * @return HTMLPurifier - * @note Backwards compatibility, see instance() - */ - public static function getInstance($prototype = null) - { - return HTMLPurifier::instance($prototype); - } -} - -// vim: et sw=4 sts=4 +config = HTMLPurifier_Config::create($config); + $this->strategy = new HTMLPurifier_Strategy_Core(); + } + + /** + * Adds a filter to process the output. First come first serve + * + * @param HTMLPurifier_Filter $filter HTMLPurifier_Filter object + */ + public function addFilter($filter) + { + trigger_error( + 'HTMLPurifier->addFilter() is deprecated, use configuration directives' . + ' in the Filter namespace or Filter.Custom', + E_USER_WARNING + ); + $this->filters[] = $filter; + } + + /** + * Filters an HTML snippet/document to be XSS-free and standards-compliant. + * + * @param string $html String of HTML to purify + * @param HTMLPurifier_Config $config Config object for this operation, + * if omitted, defaults to the config object specified during this + * object's construction. The parameter can also be any type + * that HTMLPurifier_Config::create() supports. + * + * @return string Purified HTML + */ + public function purify($html, $config = null) + { + // :TODO: make the config merge in, instead of replace + $config = $config ? HTMLPurifier_Config::create($config) : $this->config; + + // implementation is partially environment dependant, partially + // configuration dependant + $lexer = HTMLPurifier_Lexer::create($config); + + $context = new HTMLPurifier_Context(); + + // setup HTML generator + $this->generator = new HTMLPurifier_Generator($config, $context); + $context->register('Generator', $this->generator); + + // set up global context variables + if ($config->get('Core.CollectErrors')) { + // may get moved out if other facilities use it + $language_factory = HTMLPurifier_LanguageFactory::instance(); + $language = $language_factory->create($config, $context); + $context->register('Locale', $language); + + $error_collector = new HTMLPurifier_ErrorCollector($context); + $context->register('ErrorCollector', $error_collector); + } + + // setup id_accumulator context, necessary due to the fact that + // AttrValidator can be called from many places + $id_accumulator = HTMLPurifier_IDAccumulator::build($config, $context); + $context->register('IDAccumulator', $id_accumulator); + + $html = HTMLPurifier_Encoder::convertToUTF8($html, $config, $context); + + // setup filters + $filter_flags = $config->getBatch('Filter'); + $custom_filters = $filter_flags['Custom']; + unset($filter_flags['Custom']); + $filters = array(); + foreach ($filter_flags as $filter => $flag) { + if (!$flag) { + continue; + } + if (strpos($filter, '.') !== false) { + continue; + } + $class = "HTMLPurifier_Filter_$filter"; + $filters[] = new $class; + } + foreach ($custom_filters as $filter) { + // maybe "HTMLPurifier_Filter_$filter", but be consistent with AutoFormat + $filters[] = $filter; + } + $filters = array_merge($filters, $this->filters); + // maybe prepare(), but later + + for ($i = 0, $filter_size = count($filters); $i < $filter_size; $i++) { + $html = $filters[$i]->preFilter($html, $config, $context); + } + + // purified HTML + $html = + $this->generator->generateFromTokens( + // list of tokens + $this->strategy->execute( + // list of un-purified tokens + $lexer->tokenizeHTML( + // un-purified HTML + $html, + $config, + $context + ), + $config, + $context + ) + ); + + for ($i = $filter_size - 1; $i >= 0; $i--) { + $html = $filters[$i]->postFilter($html, $config, $context); + } + + $html = HTMLPurifier_Encoder::convertFromUTF8($html, $config, $context); + $this->context =& $context; + return $html; + } + + /** + * Filters an array of HTML snippets + * + * @param string[] $array_of_html Array of html snippets + * @param HTMLPurifier_Config $config Optional config object for this operation. + * See HTMLPurifier::purify() for more details. + * + * @return string[] Array of purified HTML + */ + public function purifyArray($array_of_html, $config = null) + { + $context_array = array(); + $array = array(); + foreach($array_of_html as $key=>$value){ + if (is_array($value)) { + $array[$key] = $this->purifyArray($value, $config); + } else { + $array[$key] = $this->purify($value, $config); + } + $context_array[$key] = $this->context; + } + $this->context = $context_array; + return $array; + } + + /** + * Singleton for enforcing just one HTML Purifier in your system + * + * @param HTMLPurifier|HTMLPurifier_Config $prototype Optional prototype + * HTMLPurifier instance to overload singleton with, + * or HTMLPurifier_Config instance to configure the + * generated version with. + * + * @return HTMLPurifier + */ + public static function instance($prototype = null) + { + if (!self::$instance || $prototype) { + if ($prototype instanceof HTMLPurifier) { + self::$instance = $prototype; + } elseif ($prototype) { + self::$instance = new HTMLPurifier($prototype); + } else { + self::$instance = new HTMLPurifier(); + } + } + return self::$instance; + } + + /** + * Singleton for enforcing just one HTML Purifier in your system + * + * @param HTMLPurifier|HTMLPurifier_Config $prototype Optional prototype + * HTMLPurifier instance to overload singleton with, + * or HTMLPurifier_Config instance to configure the + * generated version with. + * + * @return HTMLPurifier + * @note Backwards compatibility, see instance() + */ + public static function getInstance($prototype = null) + { + return HTMLPurifier::instance($prototype); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier.safe-includes.php b/lib/purifier/HTMLPurifier.safe-includes.php index 852a0b853c..a3261f8a32 100644 --- a/lib/purifier/HTMLPurifier.safe-includes.php +++ b/lib/purifier/HTMLPurifier.safe-includes.php @@ -1,228 +1,228 @@ -getHTMLDefinition(); - $parent = new HTMLPurifier_Token_Start($definition->info_parent); - $stack = array($parent->toNode()); - foreach ($tokens as $token) { - $token->skip = null; // [MUT] - $token->carryover = null; // [MUT] - if ($token instanceof HTMLPurifier_Token_End) { - $token->start = null; // [MUT] - $r = array_pop($stack); - //assert($r->name === $token->name); - //assert(empty($token->attr)); - $r->endCol = $token->col; - $r->endLine = $token->line; - $r->endArmor = $token->armor; - continue; - } - $node = $token->toNode(); - $stack[count($stack)-1]->children[] = $node; - if ($token instanceof HTMLPurifier_Token_Start) { - $stack[] = $node; - } - } - //assert(count($stack) == 1); - return $stack[0]; - } - - public static function flatten($node, $config, $context) { - $level = 0; - $nodes = array($level => new HTMLPurifier_Queue(array($node))); - $closingTokens = array(); - $tokens = array(); - do { - while (!$nodes[$level]->isEmpty()) { - $node = $nodes[$level]->shift(); // FIFO - list($start, $end) = $node->toTokenPair(); - if ($level > 0) { - $tokens[] = $start; - } - if ($end !== NULL) { - $closingTokens[$level][] = $end; - } - if ($node instanceof HTMLPurifier_Node_Element) { - $level++; - $nodes[$level] = new HTMLPurifier_Queue(); - foreach ($node->children as $childNode) { - $nodes[$level]->push($childNode); - } - } - } - $level--; - if ($level && isset($closingTokens[$level])) { - while ($token = array_pop($closingTokens[$level])) { - $tokens[] = $token; - } - } - } while ($level > 0); - return $tokens; - } -} +getHTMLDefinition(); + $parent = new HTMLPurifier_Token_Start($definition->info_parent); + $stack = array($parent->toNode()); + foreach ($tokens as $token) { + $token->skip = null; // [MUT] + $token->carryover = null; // [MUT] + if ($token instanceof HTMLPurifier_Token_End) { + $token->start = null; // [MUT] + $r = array_pop($stack); + //assert($r->name === $token->name); + //assert(empty($token->attr)); + $r->endCol = $token->col; + $r->endLine = $token->line; + $r->endArmor = $token->armor; + continue; + } + $node = $token->toNode(); + $stack[count($stack)-1]->children[] = $node; + if ($token instanceof HTMLPurifier_Token_Start) { + $stack[] = $node; + } + } + //assert(count($stack) == 1); + return $stack[0]; + } + + public static function flatten($node, $config, $context) { + $level = 0; + $nodes = array($level => new HTMLPurifier_Queue(array($node))); + $closingTokens = array(); + $tokens = array(); + do { + while (!$nodes[$level]->isEmpty()) { + $node = $nodes[$level]->shift(); // FIFO + list($start, $end) = $node->toTokenPair(); + if ($level > 0) { + $tokens[] = $start; + } + if ($end !== NULL) { + $closingTokens[$level][] = $end; + } + if ($node instanceof HTMLPurifier_Node_Element) { + $level++; + $nodes[$level] = new HTMLPurifier_Queue(); + foreach ($node->children as $childNode) { + $nodes[$level]->push($childNode); + } + } + } + $level--; + if ($level && isset($closingTokens[$level])) { + while ($token = array_pop($closingTokens[$level])) { + $tokens[] = $token; + } + } + } while ($level > 0); + return $tokens; + } +} diff --git a/lib/purifier/HTMLPurifier/AttrCollections.php b/lib/purifier/HTMLPurifier/AttrCollections.php index 6e58663ac4..c7b17cf144 100644 --- a/lib/purifier/HTMLPurifier/AttrCollections.php +++ b/lib/purifier/HTMLPurifier/AttrCollections.php @@ -1,148 +1,148 @@ -doConstruct($attr_types, $modules); - } - - public function doConstruct($attr_types, $modules) - { - // load extensions from the modules - foreach ($modules as $module) { - foreach ($module->attr_collections as $coll_i => $coll) { - if (!isset($this->info[$coll_i])) { - $this->info[$coll_i] = array(); - } - foreach ($coll as $attr_i => $attr) { - if ($attr_i === 0 && isset($this->info[$coll_i][$attr_i])) { - // merge in includes - $this->info[$coll_i][$attr_i] = array_merge( - $this->info[$coll_i][$attr_i], - $attr - ); - continue; - } - $this->info[$coll_i][$attr_i] = $attr; - } - } - } - // perform internal expansions and inclusions - foreach ($this->info as $name => $attr) { - // merge attribute collections that include others - $this->performInclusions($this->info[$name]); - // replace string identifiers with actual attribute objects - $this->expandIdentifiers($this->info[$name], $attr_types); - } - } - - /** - * Takes a reference to an attribute associative array and performs - * all inclusions specified by the zero index. - * @param array &$attr Reference to attribute array - */ - public function performInclusions(&$attr) - { - if (!isset($attr[0])) { - return; - } - $merge = $attr[0]; - $seen = array(); // recursion guard - // loop through all the inclusions - for ($i = 0; isset($merge[$i]); $i++) { - if (isset($seen[$merge[$i]])) { - continue; - } - $seen[$merge[$i]] = true; - // foreach attribute of the inclusion, copy it over - if (!isset($this->info[$merge[$i]])) { - continue; - } - foreach ($this->info[$merge[$i]] as $key => $value) { - if (isset($attr[$key])) { - continue; - } // also catches more inclusions - $attr[$key] = $value; - } - if (isset($this->info[$merge[$i]][0])) { - // recursion - $merge = array_merge($merge, $this->info[$merge[$i]][0]); - } - } - unset($attr[0]); - } - - /** - * Expands all string identifiers in an attribute array by replacing - * them with the appropriate values inside HTMLPurifier_AttrTypes - * @param array &$attr Reference to attribute array - * @param HTMLPurifier_AttrTypes $attr_types HTMLPurifier_AttrTypes instance - */ - public function expandIdentifiers(&$attr, $attr_types) - { - // because foreach will process new elements we add, make sure we - // skip duplicates - $processed = array(); - - foreach ($attr as $def_i => $def) { - // skip inclusions - if ($def_i === 0) { - continue; - } - - if (isset($processed[$def_i])) { - continue; - } - - // determine whether or not attribute is required - if ($required = (strpos($def_i, '*') !== false)) { - // rename the definition - unset($attr[$def_i]); - $def_i = trim($def_i, '*'); - $attr[$def_i] = $def; - } - - $processed[$def_i] = true; - - // if we've already got a literal object, move on - if (is_object($def)) { - // preserve previous required - $attr[$def_i]->required = ($required || $attr[$def_i]->required); - continue; - } - - if ($def === false) { - unset($attr[$def_i]); - continue; - } - - if ($t = $attr_types->get($def)) { - $attr[$def_i] = $t; - $attr[$def_i]->required = $required; - } else { - unset($attr[$def_i]); - } - } - } -} - -// vim: et sw=4 sts=4 +doConstruct($attr_types, $modules); + } + + public function doConstruct($attr_types, $modules) + { + // load extensions from the modules + foreach ($modules as $module) { + foreach ($module->attr_collections as $coll_i => $coll) { + if (!isset($this->info[$coll_i])) { + $this->info[$coll_i] = array(); + } + foreach ($coll as $attr_i => $attr) { + if ($attr_i === 0 && isset($this->info[$coll_i][$attr_i])) { + // merge in includes + $this->info[$coll_i][$attr_i] = array_merge( + $this->info[$coll_i][$attr_i], + $attr + ); + continue; + } + $this->info[$coll_i][$attr_i] = $attr; + } + } + } + // perform internal expansions and inclusions + foreach ($this->info as $name => $attr) { + // merge attribute collections that include others + $this->performInclusions($this->info[$name]); + // replace string identifiers with actual attribute objects + $this->expandIdentifiers($this->info[$name], $attr_types); + } + } + + /** + * Takes a reference to an attribute associative array and performs + * all inclusions specified by the zero index. + * @param array &$attr Reference to attribute array + */ + public function performInclusions(&$attr) + { + if (!isset($attr[0])) { + return; + } + $merge = $attr[0]; + $seen = array(); // recursion guard + // loop through all the inclusions + for ($i = 0; isset($merge[$i]); $i++) { + if (isset($seen[$merge[$i]])) { + continue; + } + $seen[$merge[$i]] = true; + // foreach attribute of the inclusion, copy it over + if (!isset($this->info[$merge[$i]])) { + continue; + } + foreach ($this->info[$merge[$i]] as $key => $value) { + if (isset($attr[$key])) { + continue; + } // also catches more inclusions + $attr[$key] = $value; + } + if (isset($this->info[$merge[$i]][0])) { + // recursion + $merge = array_merge($merge, $this->info[$merge[$i]][0]); + } + } + unset($attr[0]); + } + + /** + * Expands all string identifiers in an attribute array by replacing + * them with the appropriate values inside HTMLPurifier_AttrTypes + * @param array &$attr Reference to attribute array + * @param HTMLPurifier_AttrTypes $attr_types HTMLPurifier_AttrTypes instance + */ + public function expandIdentifiers(&$attr, $attr_types) + { + // because foreach will process new elements we add, make sure we + // skip duplicates + $processed = array(); + + foreach ($attr as $def_i => $def) { + // skip inclusions + if ($def_i === 0) { + continue; + } + + if (isset($processed[$def_i])) { + continue; + } + + // determine whether or not attribute is required + if ($required = (strpos($def_i, '*') !== false)) { + // rename the definition + unset($attr[$def_i]); + $def_i = trim($def_i, '*'); + $attr[$def_i] = $def; + } + + $processed[$def_i] = true; + + // if we've already got a literal object, move on + if (is_object($def)) { + // preserve previous required + $attr[$def_i]->required = ($required || $attr[$def_i]->required); + continue; + } + + if ($def === false) { + unset($attr[$def_i]); + continue; + } + + if ($t = $attr_types->get($def)) { + $attr[$def_i] = $t; + $attr[$def_i]->required = $required; + } else { + unset($attr[$def_i]); + } + } + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef.php b/lib/purifier/HTMLPurifier/AttrDef.php index 3e30d1e3b9..739646fa7c 100644 --- a/lib/purifier/HTMLPurifier/AttrDef.php +++ b/lib/purifier/HTMLPurifier/AttrDef.php @@ -1,144 +1,144 @@ - by removing - * leading and trailing whitespace, ignoring line feeds, and replacing - * carriage returns and tabs with spaces. While most useful for HTML - * attributes specified as CDATA, it can also be applied to most CSS - * values. - * - * @note This method is not entirely standards compliant, as trim() removes - * more types of whitespace than specified in the spec. In practice, - * this is rarely a problem, as those extra characters usually have - * already been removed by HTMLPurifier_Encoder. - * - * @warning This processing is inconsistent with XML's whitespace handling - * as specified by section 3.3.3 and referenced XHTML 1.0 section - * 4.7. However, note that we are NOT necessarily - * parsing XML, thus, this behavior may still be correct. We - * assume that newlines have been normalized. - */ - public function parseCDATA($string) - { - $string = trim($string); - $string = str_replace(array("\n", "\t", "\r"), ' ', $string); - return $string; - } - - /** - * Factory method for creating this class from a string. - * @param string $string String construction info - * @return HTMLPurifier_AttrDef Created AttrDef object corresponding to $string - */ - public function make($string) - { - // default implementation, return a flyweight of this object. - // If $string has an effect on the returned object (i.e. you - // need to overload this method), it is best - // to clone or instantiate new copies. (Instantiation is safer.) - return $this; - } - - /** - * Removes spaces from rgb(0, 0, 0) so that shorthand CSS properties work - * properly. THIS IS A HACK! - * @param string $string a CSS colour definition - * @return string - */ - protected function mungeRgb($string) - { - $p = '\s*(\d+(\.\d+)?([%]?))\s*'; - - if (preg_match('/(rgba|hsla)\(/', $string)) { - return preg_replace('/(rgba|hsla)\('.$p.','.$p.','.$p.','.$p.'\)/', '\1(\2,\5,\8,\11)', $string); - } - - return preg_replace('/(rgb|hsl)\('.$p.','.$p.','.$p.'\)/', '\1(\2,\5,\8)', $string); - } - - /** - * Parses a possibly escaped CSS string and returns the "pure" - * version of it. - */ - protected function expandCSSEscape($string) - { - // flexibly parse it - $ret = ''; - for ($i = 0, $c = strlen($string); $i < $c; $i++) { - if ($string[$i] === '\\') { - $i++; - if ($i >= $c) { - $ret .= '\\'; - break; - } - if (ctype_xdigit($string[$i])) { - $code = $string[$i]; - for ($a = 1, $i++; $i < $c && $a < 6; $i++, $a++) { - if (!ctype_xdigit($string[$i])) { - break; - } - $code .= $string[$i]; - } - // We have to be extremely careful when adding - // new characters, to make sure we're not breaking - // the encoding. - $char = HTMLPurifier_Encoder::unichr(hexdec($code)); - if (HTMLPurifier_Encoder::cleanUTF8($char) === '') { - continue; - } - $ret .= $char; - if ($i < $c && trim($string[$i]) !== '') { - $i--; - } - continue; - } - if ($string[$i] === "\n") { - continue; - } - } - $ret .= $string[$i]; - } - return $ret; - } -} - -// vim: et sw=4 sts=4 + by removing + * leading and trailing whitespace, ignoring line feeds, and replacing + * carriage returns and tabs with spaces. While most useful for HTML + * attributes specified as CDATA, it can also be applied to most CSS + * values. + * + * @note This method is not entirely standards compliant, as trim() removes + * more types of whitespace than specified in the spec. In practice, + * this is rarely a problem, as those extra characters usually have + * already been removed by HTMLPurifier_Encoder. + * + * @warning This processing is inconsistent with XML's whitespace handling + * as specified by section 3.3.3 and referenced XHTML 1.0 section + * 4.7. However, note that we are NOT necessarily + * parsing XML, thus, this behavior may still be correct. We + * assume that newlines have been normalized. + */ + public function parseCDATA($string) + { + $string = trim($string); + $string = str_replace(array("\n", "\t", "\r"), ' ', $string); + return $string; + } + + /** + * Factory method for creating this class from a string. + * @param string $string String construction info + * @return HTMLPurifier_AttrDef Created AttrDef object corresponding to $string + */ + public function make($string) + { + // default implementation, return a flyweight of this object. + // If $string has an effect on the returned object (i.e. you + // need to overload this method), it is best + // to clone or instantiate new copies. (Instantiation is safer.) + return $this; + } + + /** + * Removes spaces from rgb(0, 0, 0) so that shorthand CSS properties work + * properly. THIS IS A HACK! + * @param string $string a CSS colour definition + * @return string + */ + protected function mungeRgb($string) + { + $p = '\s*(\d+(\.\d+)?([%]?))\s*'; + + if (preg_match('/(rgba|hsla)\(/', $string)) { + return preg_replace('/(rgba|hsla)\('.$p.','.$p.','.$p.','.$p.'\)/', '\1(\2,\5,\8,\11)', $string); + } + + return preg_replace('/(rgb|hsl)\('.$p.','.$p.','.$p.'\)/', '\1(\2,\5,\8)', $string); + } + + /** + * Parses a possibly escaped CSS string and returns the "pure" + * version of it. + */ + protected function expandCSSEscape($string) + { + // flexibly parse it + $ret = ''; + for ($i = 0, $c = strlen($string); $i < $c; $i++) { + if ($string[$i] === '\\') { + $i++; + if ($i >= $c) { + $ret .= '\\'; + break; + } + if (ctype_xdigit($string[$i])) { + $code = $string[$i]; + for ($a = 1, $i++; $i < $c && $a < 6; $i++, $a++) { + if (!ctype_xdigit($string[$i])) { + break; + } + $code .= $string[$i]; + } + // We have to be extremely careful when adding + // new characters, to make sure we're not breaking + // the encoding. + $char = HTMLPurifier_Encoder::unichr(hexdec($code)); + if (HTMLPurifier_Encoder::cleanUTF8($char) === '') { + continue; + } + $ret .= $char; + if ($i < $c && trim($string[$i]) !== '') { + $i--; + } + continue; + } + if ($string[$i] === "\n") { + continue; + } + } + $ret .= $string[$i]; + } + return $ret; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/CSS.php b/lib/purifier/HTMLPurifier/AttrDef/CSS.php index 369db1e767..ad2cb90ad1 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/CSS.php +++ b/lib/purifier/HTMLPurifier/AttrDef/CSS.php @@ -1,136 +1,136 @@ -parseCDATA($css); - - $definition = $config->getCSSDefinition(); - $allow_duplicates = $config->get("CSS.AllowDuplicates"); - - - // According to the CSS2.1 spec, the places where a - // non-delimiting semicolon can appear are in strings - // escape sequences. So here is some dumb hack to - // handle quotes. - $len = strlen($css); - $accum = ""; - $declarations = array(); - $quoted = false; - for ($i = 0; $i < $len; $i++) { - $c = strcspn($css, ";'\"", $i); - $accum .= substr($css, $i, $c); - $i += $c; - if ($i == $len) break; - $d = $css[$i]; - if ($quoted) { - $accum .= $d; - if ($d == $quoted) { - $quoted = false; - } - } else { - if ($d == ";") { - $declarations[] = $accum; - $accum = ""; - } else { - $accum .= $d; - $quoted = $d; - } - } - } - if ($accum != "") $declarations[] = $accum; - - $propvalues = array(); - $new_declarations = ''; - - /** - * Name of the current CSS property being validated. - */ - $property = false; - $context->register('CurrentCSSProperty', $property); - - foreach ($declarations as $declaration) { - if (!$declaration) { - continue; - } - if (!strpos($declaration, ':')) { - continue; - } - list($property, $value) = explode(':', $declaration, 2); - $property = trim($property); - $value = trim($value); - $ok = false; - do { - if (isset($definition->info[$property])) { - $ok = true; - break; - } - if (ctype_lower($property)) { - break; - } - $property = strtolower($property); - if (isset($definition->info[$property])) { - $ok = true; - break; - } - } while (0); - if (!$ok) { - continue; - } - // inefficient call, since the validator will do this again - if (strtolower(trim($value)) !== 'inherit') { - // inherit works for everything (but only on the base property) - $result = $definition->info[$property]->validate( - $value, - $config, - $context - ); - } else { - $result = 'inherit'; - } - if ($result === false) { - continue; - } - if ($allow_duplicates) { - $new_declarations .= "$property:$result;"; - } else { - $propvalues[$property] = $result; - } - } - - $context->destroy('CurrentCSSProperty'); - - // procedure does not write the new CSS simultaneously, so it's - // slightly inefficient, but it's the only way of getting rid of - // duplicates. Perhaps config to optimize it, but not now. - - foreach ($propvalues as $prop => $value) { - $new_declarations .= "$prop:$value;"; - } - - return $new_declarations ? $new_declarations : false; - - } - -} - -// vim: et sw=4 sts=4 +parseCDATA($css); + + $definition = $config->getCSSDefinition(); + $allow_duplicates = $config->get("CSS.AllowDuplicates"); + + + // According to the CSS2.1 spec, the places where a + // non-delimiting semicolon can appear are in strings + // escape sequences. So here is some dumb hack to + // handle quotes. + $len = strlen($css); + $accum = ""; + $declarations = array(); + $quoted = false; + for ($i = 0; $i < $len; $i++) { + $c = strcspn($css, ";'\"", $i); + $accum .= substr($css, $i, $c); + $i += $c; + if ($i == $len) break; + $d = $css[$i]; + if ($quoted) { + $accum .= $d; + if ($d == $quoted) { + $quoted = false; + } + } else { + if ($d == ";") { + $declarations[] = $accum; + $accum = ""; + } else { + $accum .= $d; + $quoted = $d; + } + } + } + if ($accum != "") $declarations[] = $accum; + + $propvalues = array(); + $new_declarations = ''; + + /** + * Name of the current CSS property being validated. + */ + $property = false; + $context->register('CurrentCSSProperty', $property); + + foreach ($declarations as $declaration) { + if (!$declaration) { + continue; + } + if (!strpos($declaration, ':')) { + continue; + } + list($property, $value) = explode(':', $declaration, 2); + $property = trim($property); + $value = trim($value); + $ok = false; + do { + if (isset($definition->info[$property])) { + $ok = true; + break; + } + if (ctype_lower($property)) { + break; + } + $property = strtolower($property); + if (isset($definition->info[$property])) { + $ok = true; + break; + } + } while (0); + if (!$ok) { + continue; + } + // inefficient call, since the validator will do this again + if (strtolower(trim($value)) !== 'inherit') { + // inherit works for everything (but only on the base property) + $result = $definition->info[$property]->validate( + $value, + $config, + $context + ); + } else { + $result = 'inherit'; + } + if ($result === false) { + continue; + } + if ($allow_duplicates) { + $new_declarations .= "$property:$result;"; + } else { + $propvalues[$property] = $result; + } + } + + $context->destroy('CurrentCSSProperty'); + + // procedure does not write the new CSS simultaneously, so it's + // slightly inefficient, but it's the only way of getting rid of + // duplicates. Perhaps config to optimize it, but not now. + + foreach ($propvalues as $prop => $value) { + $new_declarations .= "$prop:$value;"; + } + + return $new_declarations ? $new_declarations : false; + + } + +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/CSS/AlphaValue.php b/lib/purifier/HTMLPurifier/AttrDef/CSS/AlphaValue.php index 1a30e8fe0c..af2b83dff8 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/CSS/AlphaValue.php +++ b/lib/purifier/HTMLPurifier/AttrDef/CSS/AlphaValue.php @@ -1,34 +1,34 @@ - 1.0) { - $result = '1'; - } - return $result; - } -} - -// vim: et sw=4 sts=4 + 1.0) { + $result = '1'; + } + return $result; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/CSS/Background.php b/lib/purifier/HTMLPurifier/AttrDef/CSS/Background.php index ecd6e276e9..7f1ea3b0f1 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/CSS/Background.php +++ b/lib/purifier/HTMLPurifier/AttrDef/CSS/Background.php @@ -1,111 +1,111 @@ -getCSSDefinition(); - $this->info['background-color'] = $def->info['background-color']; - $this->info['background-image'] = $def->info['background-image']; - $this->info['background-repeat'] = $def->info['background-repeat']; - $this->info['background-attachment'] = $def->info['background-attachment']; - $this->info['background-position'] = $def->info['background-position']; - } - - /** - * @param string $string - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool|string - */ - public function validate($string, $config, $context) - { - // regular pre-processing - $string = $this->parseCDATA($string); - if ($string === '') { - return false; - } - - // munge rgb() decl if necessary - $string = $this->mungeRgb($string); - - // assumes URI doesn't have spaces in it - $bits = explode(' ', $string); // bits to process - - $caught = array(); - $caught['color'] = false; - $caught['image'] = false; - $caught['repeat'] = false; - $caught['attachment'] = false; - $caught['position'] = false; - - $i = 0; // number of catches - - foreach ($bits as $bit) { - if ($bit === '') { - continue; - } - foreach ($caught as $key => $status) { - if ($key != 'position') { - if ($status !== false) { - continue; - } - $r = $this->info['background-' . $key]->validate($bit, $config, $context); - } else { - $r = $bit; - } - if ($r === false) { - continue; - } - if ($key == 'position') { - if ($caught[$key] === false) { - $caught[$key] = ''; - } - $caught[$key] .= $r . ' '; - } else { - $caught[$key] = $r; - } - $i++; - break; - } - } - - if (!$i) { - return false; - } - if ($caught['position'] !== false) { - $caught['position'] = $this->info['background-position']-> - validate($caught['position'], $config, $context); - } - - $ret = array(); - foreach ($caught as $value) { - if ($value === false) { - continue; - } - $ret[] = $value; - } - - if (empty($ret)) { - return false; - } - return implode(' ', $ret); - } -} - -// vim: et sw=4 sts=4 +getCSSDefinition(); + $this->info['background-color'] = $def->info['background-color']; + $this->info['background-image'] = $def->info['background-image']; + $this->info['background-repeat'] = $def->info['background-repeat']; + $this->info['background-attachment'] = $def->info['background-attachment']; + $this->info['background-position'] = $def->info['background-position']; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + // regular pre-processing + $string = $this->parseCDATA($string); + if ($string === '') { + return false; + } + + // munge rgb() decl if necessary + $string = $this->mungeRgb($string); + + // assumes URI doesn't have spaces in it + $bits = explode(' ', $string); // bits to process + + $caught = array(); + $caught['color'] = false; + $caught['image'] = false; + $caught['repeat'] = false; + $caught['attachment'] = false; + $caught['position'] = false; + + $i = 0; // number of catches + + foreach ($bits as $bit) { + if ($bit === '') { + continue; + } + foreach ($caught as $key => $status) { + if ($key != 'position') { + if ($status !== false) { + continue; + } + $r = $this->info['background-' . $key]->validate($bit, $config, $context); + } else { + $r = $bit; + } + if ($r === false) { + continue; + } + if ($key == 'position') { + if ($caught[$key] === false) { + $caught[$key] = ''; + } + $caught[$key] .= $r . ' '; + } else { + $caught[$key] = $r; + } + $i++; + break; + } + } + + if (!$i) { + return false; + } + if ($caught['position'] !== false) { + $caught['position'] = $this->info['background-position']-> + validate($caught['position'], $config, $context); + } + + $ret = array(); + foreach ($caught as $value) { + if ($value === false) { + continue; + } + $ret[] = $value; + } + + if (empty($ret)) { + return false; + } + return implode(' ', $ret); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/CSS/BackgroundPosition.php b/lib/purifier/HTMLPurifier/AttrDef/CSS/BackgroundPosition.php index f95de5bbfe..4580ef5a91 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/CSS/BackgroundPosition.php +++ b/lib/purifier/HTMLPurifier/AttrDef/CSS/BackgroundPosition.php @@ -1,157 +1,157 @@ - | | left | center | right - ] - [ - | | top | center | bottom - ]? - ] | - [ // this signifies that the vertical and horizontal adjectives - // can be arbitrarily ordered, however, there can only be two, - // one of each, or none at all - [ - left | center | right - ] || - [ - top | center | bottom - ] - ] - top, left = 0% - center, (none) = 50% - bottom, right = 100% -*/ - -/* QuirksMode says: - keyword + length/percentage must be ordered correctly, as per W3C - - Internet Explorer and Opera, however, support arbitrary ordering. We - should fix it up. - - Minor issue though, not strictly necessary. -*/ - -// control freaks may appreciate the ability to convert these to -// percentages or something, but it's not necessary - -/** - * Validates the value of background-position. - */ -class HTMLPurifier_AttrDef_CSS_BackgroundPosition extends HTMLPurifier_AttrDef -{ - - /** - * @type HTMLPurifier_AttrDef_CSS_Length - */ - protected $length; - - /** - * @type HTMLPurifier_AttrDef_CSS_Percentage - */ - protected $percentage; - - public function __construct() - { - $this->length = new HTMLPurifier_AttrDef_CSS_Length(); - $this->percentage = new HTMLPurifier_AttrDef_CSS_Percentage(); - } - - /** - * @param string $string - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool|string - */ - public function validate($string, $config, $context) - { - $string = $this->parseCDATA($string); - $bits = explode(' ', $string); - - $keywords = array(); - $keywords['h'] = false; // left, right - $keywords['v'] = false; // top, bottom - $keywords['ch'] = false; // center (first word) - $keywords['cv'] = false; // center (second word) - $measures = array(); - - $i = 0; - - $lookup = array( - 'top' => 'v', - 'bottom' => 'v', - 'left' => 'h', - 'right' => 'h', - 'center' => 'c' - ); - - foreach ($bits as $bit) { - if ($bit === '') { - continue; - } - - // test for keyword - $lbit = ctype_lower($bit) ? $bit : strtolower($bit); - if (isset($lookup[$lbit])) { - $status = $lookup[$lbit]; - if ($status == 'c') { - if ($i == 0) { - $status = 'ch'; - } else { - $status = 'cv'; - } - } - $keywords[$status] = $lbit; - $i++; - } - - // test for length - $r = $this->length->validate($bit, $config, $context); - if ($r !== false) { - $measures[] = $r; - $i++; - } - - // test for percentage - $r = $this->percentage->validate($bit, $config, $context); - if ($r !== false) { - $measures[] = $r; - $i++; - } - } - - if (!$i) { - return false; - } // no valid values were caught - - $ret = array(); - - // first keyword - if ($keywords['h']) { - $ret[] = $keywords['h']; - } elseif ($keywords['ch']) { - $ret[] = $keywords['ch']; - $keywords['cv'] = false; // prevent re-use: center = center center - } elseif (count($measures)) { - $ret[] = array_shift($measures); - } - - if ($keywords['v']) { - $ret[] = $keywords['v']; - } elseif ($keywords['cv']) { - $ret[] = $keywords['cv']; - } elseif (count($measures)) { - $ret[] = array_shift($measures); - } - - if (empty($ret)) { - return false; - } - return implode(' ', $ret); - } -} - -// vim: et sw=4 sts=4 + | | left | center | right + ] + [ + | | top | center | bottom + ]? + ] | + [ // this signifies that the vertical and horizontal adjectives + // can be arbitrarily ordered, however, there can only be two, + // one of each, or none at all + [ + left | center | right + ] || + [ + top | center | bottom + ] + ] + top, left = 0% + center, (none) = 50% + bottom, right = 100% +*/ + +/* QuirksMode says: + keyword + length/percentage must be ordered correctly, as per W3C + + Internet Explorer and Opera, however, support arbitrary ordering. We + should fix it up. + + Minor issue though, not strictly necessary. +*/ + +// control freaks may appreciate the ability to convert these to +// percentages or something, but it's not necessary + +/** + * Validates the value of background-position. + */ +class HTMLPurifier_AttrDef_CSS_BackgroundPosition extends HTMLPurifier_AttrDef +{ + + /** + * @type HTMLPurifier_AttrDef_CSS_Length + */ + protected $length; + + /** + * @type HTMLPurifier_AttrDef_CSS_Percentage + */ + protected $percentage; + + public function __construct() + { + $this->length = new HTMLPurifier_AttrDef_CSS_Length(); + $this->percentage = new HTMLPurifier_AttrDef_CSS_Percentage(); + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $string = $this->parseCDATA($string); + $bits = explode(' ', $string); + + $keywords = array(); + $keywords['h'] = false; // left, right + $keywords['v'] = false; // top, bottom + $keywords['ch'] = false; // center (first word) + $keywords['cv'] = false; // center (second word) + $measures = array(); + + $i = 0; + + $lookup = array( + 'top' => 'v', + 'bottom' => 'v', + 'left' => 'h', + 'right' => 'h', + 'center' => 'c' + ); + + foreach ($bits as $bit) { + if ($bit === '') { + continue; + } + + // test for keyword + $lbit = ctype_lower($bit) ? $bit : strtolower($bit); + if (isset($lookup[$lbit])) { + $status = $lookup[$lbit]; + if ($status == 'c') { + if ($i == 0) { + $status = 'ch'; + } else { + $status = 'cv'; + } + } + $keywords[$status] = $lbit; + $i++; + } + + // test for length + $r = $this->length->validate($bit, $config, $context); + if ($r !== false) { + $measures[] = $r; + $i++; + } + + // test for percentage + $r = $this->percentage->validate($bit, $config, $context); + if ($r !== false) { + $measures[] = $r; + $i++; + } + } + + if (!$i) { + return false; + } // no valid values were caught + + $ret = array(); + + // first keyword + if ($keywords['h']) { + $ret[] = $keywords['h']; + } elseif ($keywords['ch']) { + $ret[] = $keywords['ch']; + $keywords['cv'] = false; // prevent re-use: center = center center + } elseif (count($measures)) { + $ret[] = array_shift($measures); + } + + if ($keywords['v']) { + $ret[] = $keywords['v']; + } elseif ($keywords['cv']) { + $ret[] = $keywords['cv']; + } elseif (count($measures)) { + $ret[] = array_shift($measures); + } + + if (empty($ret)) { + return false; + } + return implode(' ', $ret); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/CSS/Border.php b/lib/purifier/HTMLPurifier/AttrDef/CSS/Border.php index bd310ff231..16243ba1ed 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/CSS/Border.php +++ b/lib/purifier/HTMLPurifier/AttrDef/CSS/Border.php @@ -1,56 +1,56 @@ -getCSSDefinition(); - $this->info['border-width'] = $def->info['border-width']; - $this->info['border-style'] = $def->info['border-style']; - $this->info['border-top-color'] = $def->info['border-top-color']; - } - - /** - * @param string $string - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool|string - */ - public function validate($string, $config, $context) - { - $string = $this->parseCDATA($string); - $string = $this->mungeRgb($string); - $bits = explode(' ', $string); - $done = array(); // segments we've finished - $ret = ''; // return value - foreach ($bits as $bit) { - foreach ($this->info as $propname => $validator) { - if (isset($done[$propname])) { - continue; - } - $r = $validator->validate($bit, $config, $context); - if ($r !== false) { - $ret .= $r . ' '; - $done[$propname] = true; - break; - } - } - } - return rtrim($ret); - } -} - -// vim: et sw=4 sts=4 +getCSSDefinition(); + $this->info['border-width'] = $def->info['border-width']; + $this->info['border-style'] = $def->info['border-style']; + $this->info['border-top-color'] = $def->info['border-top-color']; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $string = $this->parseCDATA($string); + $string = $this->mungeRgb($string); + $bits = explode(' ', $string); + $done = array(); // segments we've finished + $ret = ''; // return value + foreach ($bits as $bit) { + foreach ($this->info as $propname => $validator) { + if (isset($done[$propname])) { + continue; + } + $r = $validator->validate($bit, $config, $context); + if ($r !== false) { + $ret .= $r . ' '; + $done[$propname] = true; + break; + } + } + } + return rtrim($ret); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/CSS/Color.php b/lib/purifier/HTMLPurifier/AttrDef/CSS/Color.php index d1b1b3c194..d7287a00c2 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/CSS/Color.php +++ b/lib/purifier/HTMLPurifier/AttrDef/CSS/Color.php @@ -1,161 +1,161 @@ -alpha = new HTMLPurifier_AttrDef_CSS_AlphaValue(); - } - - /** - * @param string $color - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool|string - */ - public function validate($color, $config, $context) - { - static $colors = null; - if ($colors === null) { - $colors = $config->get('Core.ColorKeywords'); - } - - $color = trim($color); - if ($color === '') { - return false; - } - - $lower = strtolower($color); - if (isset($colors[$lower])) { - return $colors[$lower]; - } - - if (preg_match('#(rgb|rgba|hsl|hsla)\(#', $color, $matches) === 1) { - $length = strlen($color); - if (strpos($color, ')') !== $length - 1) { - return false; - } - - // get used function : rgb, rgba, hsl or hsla - $function = $matches[1]; - - $parameters_size = 3; - $alpha_channel = false; - if (substr($function, -1) === 'a') { - $parameters_size = 4; - $alpha_channel = true; - } - - /* - * Allowed types for values : - * parameter_position => [type => max_value] - */ - $allowed_types = array( - 1 => array('percentage' => 100, 'integer' => 255), - 2 => array('percentage' => 100, 'integer' => 255), - 3 => array('percentage' => 100, 'integer' => 255), - ); - $allow_different_types = false; - - if (strpos($function, 'hsl') !== false) { - $allowed_types = array( - 1 => array('integer' => 360), - 2 => array('percentage' => 100), - 3 => array('percentage' => 100), - ); - $allow_different_types = true; - } - - $values = trim(str_replace($function, '', $color), ' ()'); - - $parts = explode(',', $values); - if (count($parts) !== $parameters_size) { - return false; - } - - $type = false; - $new_parts = array(); - $i = 0; - - foreach ($parts as $part) { - $i++; - $part = trim($part); - - if ($part === '') { - return false; - } - - // different check for alpha channel - if ($alpha_channel === true && $i === count($parts)) { - $result = $this->alpha->validate($part, $config, $context); - - if ($result === false) { - return false; - } - - $new_parts[] = (string)$result; - continue; - } - - if (substr($part, -1) === '%') { - $current_type = 'percentage'; - } else { - $current_type = 'integer'; - } - - if (!array_key_exists($current_type, $allowed_types[$i])) { - return false; - } - - if (!$type) { - $type = $current_type; - } - - if ($allow_different_types === false && $type != $current_type) { - return false; - } - - $max_value = $allowed_types[$i][$current_type]; - - if ($current_type == 'integer') { - // Return value between range 0 -> $max_value - $new_parts[] = (int)max(min($part, $max_value), 0); - } elseif ($current_type == 'percentage') { - $new_parts[] = (float)max(min(rtrim($part, '%'), $max_value), 0) . '%'; - } - } - - $new_values = implode(',', $new_parts); - - $color = $function . '(' . $new_values . ')'; - } else { - // hexadecimal handling - if ($color[0] === '#') { - $hex = substr($color, 1); - } else { - $hex = $color; - $color = '#' . $color; - } - $length = strlen($hex); - if ($length !== 3 && $length !== 6) { - return false; - } - if (!ctype_xdigit($hex)) { - return false; - } - } - return $color; - } - -} - -// vim: et sw=4 sts=4 +alpha = new HTMLPurifier_AttrDef_CSS_AlphaValue(); + } + + /** + * @param string $color + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($color, $config, $context) + { + static $colors = null; + if ($colors === null) { + $colors = $config->get('Core.ColorKeywords'); + } + + $color = trim($color); + if ($color === '') { + return false; + } + + $lower = strtolower($color); + if (isset($colors[$lower])) { + return $colors[$lower]; + } + + if (preg_match('#(rgb|rgba|hsl|hsla)\(#', $color, $matches) === 1) { + $length = strlen($color); + if (strpos($color, ')') !== $length - 1) { + return false; + } + + // get used function : rgb, rgba, hsl or hsla + $function = $matches[1]; + + $parameters_size = 3; + $alpha_channel = false; + if (substr($function, -1) === 'a') { + $parameters_size = 4; + $alpha_channel = true; + } + + /* + * Allowed types for values : + * parameter_position => [type => max_value] + */ + $allowed_types = array( + 1 => array('percentage' => 100, 'integer' => 255), + 2 => array('percentage' => 100, 'integer' => 255), + 3 => array('percentage' => 100, 'integer' => 255), + ); + $allow_different_types = false; + + if (strpos($function, 'hsl') !== false) { + $allowed_types = array( + 1 => array('integer' => 360), + 2 => array('percentage' => 100), + 3 => array('percentage' => 100), + ); + $allow_different_types = true; + } + + $values = trim(str_replace($function, '', $color), ' ()'); + + $parts = explode(',', $values); + if (count($parts) !== $parameters_size) { + return false; + } + + $type = false; + $new_parts = array(); + $i = 0; + + foreach ($parts as $part) { + $i++; + $part = trim($part); + + if ($part === '') { + return false; + } + + // different check for alpha channel + if ($alpha_channel === true && $i === count($parts)) { + $result = $this->alpha->validate($part, $config, $context); + + if ($result === false) { + return false; + } + + $new_parts[] = (string)$result; + continue; + } + + if (substr($part, -1) === '%') { + $current_type = 'percentage'; + } else { + $current_type = 'integer'; + } + + if (!array_key_exists($current_type, $allowed_types[$i])) { + return false; + } + + if (!$type) { + $type = $current_type; + } + + if ($allow_different_types === false && $type != $current_type) { + return false; + } + + $max_value = $allowed_types[$i][$current_type]; + + if ($current_type == 'integer') { + // Return value between range 0 -> $max_value + $new_parts[] = (int)max(min($part, $max_value), 0); + } elseif ($current_type == 'percentage') { + $new_parts[] = (float)max(min(rtrim($part, '%'), $max_value), 0) . '%'; + } + } + + $new_values = implode(',', $new_parts); + + $color = $function . '(' . $new_values . ')'; + } else { + // hexadecimal handling + if ($color[0] === '#') { + $hex = substr($color, 1); + } else { + $hex = $color; + $color = '#' . $color; + } + $length = strlen($hex); + if ($length !== 3 && $length !== 6) { + return false; + } + if (!ctype_xdigit($hex)) { + return false; + } + } + return $color; + } + +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/CSS/Composite.php b/lib/purifier/HTMLPurifier/AttrDef/CSS/Composite.php index 3890023229..9c1750554f 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/CSS/Composite.php +++ b/lib/purifier/HTMLPurifier/AttrDef/CSS/Composite.php @@ -1,48 +1,48 @@ -defs = $defs; - } - - /** - * @param string $string - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool|string - */ - public function validate($string, $config, $context) - { - foreach ($this->defs as $i => $def) { - $result = $this->defs[$i]->validate($string, $config, $context); - if ($result !== false) { - return $result; - } - } - return false; - } -} - -// vim: et sw=4 sts=4 +defs = $defs; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + foreach ($this->defs as $i => $def) { + $result = $this->defs[$i]->validate($string, $config, $context); + if ($result !== false) { + return $result; + } + } + return false; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/CSS/DenyElementDecorator.php b/lib/purifier/HTMLPurifier/AttrDef/CSS/DenyElementDecorator.php index ff0d897edc..9d77cc9aaf 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/CSS/DenyElementDecorator.php +++ b/lib/purifier/HTMLPurifier/AttrDef/CSS/DenyElementDecorator.php @@ -1,44 +1,44 @@ -def = $def; - $this->element = $element; - } - - /** - * Checks if CurrentToken is set and equal to $this->element - * @param string $string - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool|string - */ - public function validate($string, $config, $context) - { - $token = $context->get('CurrentToken', true); - if ($token && $token->name == $this->element) { - return false; - } - return $this->def->validate($string, $config, $context); - } -} - -// vim: et sw=4 sts=4 +def = $def; + $this->element = $element; + } + + /** + * Checks if CurrentToken is set and equal to $this->element + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $token = $context->get('CurrentToken', true); + if ($token && $token->name == $this->element) { + return false; + } + return $this->def->validate($string, $config, $context); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/CSS/Filter.php b/lib/purifier/HTMLPurifier/AttrDef/CSS/Filter.php index 019722a487..bde4c3301f 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/CSS/Filter.php +++ b/lib/purifier/HTMLPurifier/AttrDef/CSS/Filter.php @@ -1,77 +1,77 @@ -intValidator = new HTMLPurifier_AttrDef_Integer(); - } - - /** - * @param string $value - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool|string - */ - public function validate($value, $config, $context) - { - $value = $this->parseCDATA($value); - if ($value === 'none') { - return $value; - } - // if we looped this we could support multiple filters - $function_length = strcspn($value, '('); - $function = trim(substr($value, 0, $function_length)); - if ($function !== 'alpha' && - $function !== 'Alpha' && - $function !== 'progid:DXImageTransform.Microsoft.Alpha' - ) { - return false; - } - $cursor = $function_length + 1; - $parameters_length = strcspn($value, ')', $cursor); - $parameters = substr($value, $cursor, $parameters_length); - $params = explode(',', $parameters); - $ret_params = array(); - $lookup = array(); - foreach ($params as $param) { - list($key, $value) = explode('=', $param); - $key = trim($key); - $value = trim($value); - if (isset($lookup[$key])) { - continue; - } - if ($key !== 'opacity') { - continue; - } - $value = $this->intValidator->validate($value, $config, $context); - if ($value === false) { - continue; - } - $int = (int)$value; - if ($int > 100) { - $value = '100'; - } - if ($int < 0) { - $value = '0'; - } - $ret_params[] = "$key=$value"; - $lookup[$key] = true; - } - $ret_parameters = implode(',', $ret_params); - $ret_function = "$function($ret_parameters)"; - return $ret_function; - } -} - -// vim: et sw=4 sts=4 +intValidator = new HTMLPurifier_AttrDef_Integer(); + } + + /** + * @param string $value + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($value, $config, $context) + { + $value = $this->parseCDATA($value); + if ($value === 'none') { + return $value; + } + // if we looped this we could support multiple filters + $function_length = strcspn($value, '('); + $function = trim(substr($value, 0, $function_length)); + if ($function !== 'alpha' && + $function !== 'Alpha' && + $function !== 'progid:DXImageTransform.Microsoft.Alpha' + ) { + return false; + } + $cursor = $function_length + 1; + $parameters_length = strcspn($value, ')', $cursor); + $parameters = substr($value, $cursor, $parameters_length); + $params = explode(',', $parameters); + $ret_params = array(); + $lookup = array(); + foreach ($params as $param) { + list($key, $value) = explode('=', $param); + $key = trim($key); + $value = trim($value); + if (isset($lookup[$key])) { + continue; + } + if ($key !== 'opacity') { + continue; + } + $value = $this->intValidator->validate($value, $config, $context); + if ($value === false) { + continue; + } + $int = (int)$value; + if ($int > 100) { + $value = '100'; + } + if ($int < 0) { + $value = '0'; + } + $ret_params[] = "$key=$value"; + $lookup[$key] = true; + } + $ret_parameters = implode(',', $ret_params); + $ret_function = "$function($ret_parameters)"; + return $ret_function; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/CSS/Font.php b/lib/purifier/HTMLPurifier/AttrDef/CSS/Font.php index b9b63f8efd..579b97ef1c 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/CSS/Font.php +++ b/lib/purifier/HTMLPurifier/AttrDef/CSS/Font.php @@ -1,176 +1,176 @@ -getCSSDefinition(); - $this->info['font-style'] = $def->info['font-style']; - $this->info['font-variant'] = $def->info['font-variant']; - $this->info['font-weight'] = $def->info['font-weight']; - $this->info['font-size'] = $def->info['font-size']; - $this->info['line-height'] = $def->info['line-height']; - $this->info['font-family'] = $def->info['font-family']; - } - - /** - * @param string $string - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool|string - */ - public function validate($string, $config, $context) - { - static $system_fonts = array( - 'caption' => true, - 'icon' => true, - 'menu' => true, - 'message-box' => true, - 'small-caption' => true, - 'status-bar' => true - ); - - // regular pre-processing - $string = $this->parseCDATA($string); - if ($string === '') { - return false; - } - - // check if it's one of the keywords - $lowercase_string = strtolower($string); - if (isset($system_fonts[$lowercase_string])) { - return $lowercase_string; - } - - $bits = explode(' ', $string); // bits to process - $stage = 0; // this indicates what we're looking for - $caught = array(); // which stage 0 properties have we caught? - $stage_1 = array('font-style', 'font-variant', 'font-weight'); - $final = ''; // output - - for ($i = 0, $size = count($bits); $i < $size; $i++) { - if ($bits[$i] === '') { - continue; - } - switch ($stage) { - case 0: // attempting to catch font-style, font-variant or font-weight - foreach ($stage_1 as $validator_name) { - if (isset($caught[$validator_name])) { - continue; - } - $r = $this->info[$validator_name]->validate( - $bits[$i], - $config, - $context - ); - if ($r !== false) { - $final .= $r . ' '; - $caught[$validator_name] = true; - break; - } - } - // all three caught, continue on - if (count($caught) >= 3) { - $stage = 1; - } - if ($r !== false) { - break; - } - case 1: // attempting to catch font-size and perhaps line-height - $found_slash = false; - if (strpos($bits[$i], '/') !== false) { - list($font_size, $line_height) = - explode('/', $bits[$i]); - if ($line_height === '') { - // ooh, there's a space after the slash! - $line_height = false; - $found_slash = true; - } - } else { - $font_size = $bits[$i]; - $line_height = false; - } - $r = $this->info['font-size']->validate( - $font_size, - $config, - $context - ); - if ($r !== false) { - $final .= $r; - // attempt to catch line-height - if ($line_height === false) { - // we need to scroll forward - for ($j = $i + 1; $j < $size; $j++) { - if ($bits[$j] === '') { - continue; - } - if ($bits[$j] === '/') { - if ($found_slash) { - return false; - } else { - $found_slash = true; - continue; - } - } - $line_height = $bits[$j]; - break; - } - } else { - // slash already found - $found_slash = true; - $j = $i; - } - if ($found_slash) { - $i = $j; - $r = $this->info['line-height']->validate( - $line_height, - $config, - $context - ); - if ($r !== false) { - $final .= '/' . $r; - } - } - $final .= ' '; - $stage = 2; - break; - } - return false; - case 2: // attempting to catch font-family - $font_family = - implode(' ', array_slice($bits, $i, $size - $i)); - $r = $this->info['font-family']->validate( - $font_family, - $config, - $context - ); - if ($r !== false) { - $final .= $r . ' '; - // processing completed successfully - return rtrim($final); - } - return false; - } - } - return false; - } -} - -// vim: et sw=4 sts=4 +getCSSDefinition(); + $this->info['font-style'] = $def->info['font-style']; + $this->info['font-variant'] = $def->info['font-variant']; + $this->info['font-weight'] = $def->info['font-weight']; + $this->info['font-size'] = $def->info['font-size']; + $this->info['line-height'] = $def->info['line-height']; + $this->info['font-family'] = $def->info['font-family']; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + static $system_fonts = array( + 'caption' => true, + 'icon' => true, + 'menu' => true, + 'message-box' => true, + 'small-caption' => true, + 'status-bar' => true + ); + + // regular pre-processing + $string = $this->parseCDATA($string); + if ($string === '') { + return false; + } + + // check if it's one of the keywords + $lowercase_string = strtolower($string); + if (isset($system_fonts[$lowercase_string])) { + return $lowercase_string; + } + + $bits = explode(' ', $string); // bits to process + $stage = 0; // this indicates what we're looking for + $caught = array(); // which stage 0 properties have we caught? + $stage_1 = array('font-style', 'font-variant', 'font-weight'); + $final = ''; // output + + for ($i = 0, $size = count($bits); $i < $size; $i++) { + if ($bits[$i] === '') { + continue; + } + switch ($stage) { + case 0: // attempting to catch font-style, font-variant or font-weight + foreach ($stage_1 as $validator_name) { + if (isset($caught[$validator_name])) { + continue; + } + $r = $this->info[$validator_name]->validate( + $bits[$i], + $config, + $context + ); + if ($r !== false) { + $final .= $r . ' '; + $caught[$validator_name] = true; + break; + } + } + // all three caught, continue on + if (count($caught) >= 3) { + $stage = 1; + } + if ($r !== false) { + break; + } + case 1: // attempting to catch font-size and perhaps line-height + $found_slash = false; + if (strpos($bits[$i], '/') !== false) { + list($font_size, $line_height) = + explode('/', $bits[$i]); + if ($line_height === '') { + // ooh, there's a space after the slash! + $line_height = false; + $found_slash = true; + } + } else { + $font_size = $bits[$i]; + $line_height = false; + } + $r = $this->info['font-size']->validate( + $font_size, + $config, + $context + ); + if ($r !== false) { + $final .= $r; + // attempt to catch line-height + if ($line_height === false) { + // we need to scroll forward + for ($j = $i + 1; $j < $size; $j++) { + if ($bits[$j] === '') { + continue; + } + if ($bits[$j] === '/') { + if ($found_slash) { + return false; + } else { + $found_slash = true; + continue; + } + } + $line_height = $bits[$j]; + break; + } + } else { + // slash already found + $found_slash = true; + $j = $i; + } + if ($found_slash) { + $i = $j; + $r = $this->info['line-height']->validate( + $line_height, + $config, + $context + ); + if ($r !== false) { + $final .= '/' . $r; + } + } + $final .= ' '; + $stage = 2; + break; + } + return false; + case 2: // attempting to catch font-family + $font_family = + implode(' ', array_slice($bits, $i, $size - $i)); + $r = $this->info['font-family']->validate( + $font_family, + $config, + $context + ); + if ($r !== false) { + $final .= $r . ' '; + // processing completed successfully + return rtrim($final); + } + return false; + } + } + return false; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/CSS/FontFamily.php b/lib/purifier/HTMLPurifier/AttrDef/CSS/FontFamily.php index f9af36d790..74e24c8816 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/CSS/FontFamily.php +++ b/lib/purifier/HTMLPurifier/AttrDef/CSS/FontFamily.php @@ -1,219 +1,219 @@ -mask = '_- '; - for ($c = 'a'; $c <= 'z'; $c++) { - $this->mask .= $c; - } - for ($c = 'A'; $c <= 'Z'; $c++) { - $this->mask .= $c; - } - for ($c = '0'; $c <= '9'; $c++) { - $this->mask .= $c; - } // cast-y, but should be fine - // special bytes used by UTF-8 - for ($i = 0x80; $i <= 0xFF; $i++) { - // We don't bother excluding invalid bytes in this range, - // because the our restriction of well-formed UTF-8 will - // prevent these from ever occurring. - $this->mask .= chr($i); - } - - /* - PHP's internal strcspn implementation is - O(length of string * length of mask), making it inefficient - for large masks. However, it's still faster than - preg_match 8) - for (p = s1;;) { - spanp = s2; - do { - if (*spanp == c || p == s1_end) { - return p - s1; - } - } while (spanp++ < (s2_end - 1)); - c = *++p; - } - */ - // possible optimization: invert the mask. - } - - /** - * @param string $string - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool|string - */ - public function validate($string, $config, $context) - { - static $generic_names = array( - 'serif' => true, - 'sans-serif' => true, - 'monospace' => true, - 'fantasy' => true, - 'cursive' => true - ); - $allowed_fonts = $config->get('CSS.AllowedFonts'); - - // assume that no font names contain commas in them - $fonts = explode(',', $string); - $final = ''; - foreach ($fonts as $font) { - $font = trim($font); - if ($font === '') { - continue; - } - // match a generic name - if (isset($generic_names[$font])) { - if ($allowed_fonts === null || isset($allowed_fonts[$font])) { - $final .= $font . ', '; - } - continue; - } - // match a quoted name - if ($font[0] === '"' || $font[0] === "'") { - $length = strlen($font); - if ($length <= 2) { - continue; - } - $quote = $font[0]; - if ($font[$length - 1] !== $quote) { - continue; - } - $font = substr($font, 1, $length - 2); - } - - $font = $this->expandCSSEscape($font); - - // $font is a pure representation of the font name - - if ($allowed_fonts !== null && !isset($allowed_fonts[$font])) { - continue; - } - - if (ctype_alnum($font) && $font !== '') { - // very simple font, allow it in unharmed - $final .= $font . ', '; - continue; - } - - // bugger out on whitespace. form feed (0C) really - // shouldn't show up regardless - $font = str_replace(array("\n", "\t", "\r", "\x0C"), ' ', $font); - - // Here, there are various classes of characters which need - // to be treated differently: - // - Alphanumeric characters are essentially safe. We - // handled these above. - // - Spaces require quoting, though most parsers will do - // the right thing if there aren't any characters that - // can be misinterpreted - // - Dashes rarely occur, but they fairly unproblematic - // for parsing/rendering purposes. - // The above characters cover the majority of Western font - // names. - // - Arbitrary Unicode characters not in ASCII. Because - // most parsers give little thought to Unicode, treatment - // of these codepoints is basically uniform, even for - // punctuation-like codepoints. These characters can - // show up in non-Western pages and are supported by most - // major browsers, for example: "MS 明朝" is a - // legitimate font-name - // . See - // the CSS3 spec for more examples: - // - // You can see live samples of these on the Internet: - // - // However, most of these fonts have ASCII equivalents: - // for example, 'MS Mincho', and it's considered - // professional to use ASCII font names instead of - // Unicode font names. Thanks Takeshi Terada for - // providing this information. - // The following characters, to my knowledge, have not been - // used to name font names. - // - Single quote. While theoretically you might find a - // font name that has a single quote in its name (serving - // as an apostrophe, e.g. Dave's Scribble), I haven't - // been able to find any actual examples of this. - // Internet Explorer's cssText translation (which I - // believe is invoked by innerHTML) normalizes any - // quoting to single quotes, and fails to escape single - // quotes. (Note that this is not IE's behavior for all - // CSS properties, just some sort of special casing for - // font-family). So a single quote *cannot* be used - // safely in the font-family context if there will be an - // innerHTML/cssText translation. Note that Firefox 3.x - // does this too. - // - Double quote. In IE, these get normalized to - // single-quotes, no matter what the encoding. (Fun - // fact, in IE8, the 'content' CSS property gained - // support, where they special cased to preserve encoded - // double quotes, but still translate unadorned double - // quotes into single quotes.) So, because their - // fixpoint behavior is identical to single quotes, they - // cannot be allowed either. Firefox 3.x displays - // single-quote style behavior. - // - Backslashes are reduced by one (so \\ -> \) every - // iteration, so they cannot be used safely. This shows - // up in IE7, IE8 and FF3 - // - Semicolons, commas and backticks are handled properly. - // - The rest of the ASCII punctuation is handled properly. - // We haven't checked what browsers do to unadorned - // versions, but this is not important as long as the - // browser doesn't /remove/ surrounding quotes (as IE does - // for HTML). - // - // With these results in hand, we conclude that there are - // various levels of safety: - // - Paranoid: alphanumeric, spaces and dashes(?) - // - International: Paranoid + non-ASCII Unicode - // - Edgy: Everything except quotes, backslashes - // - NoJS: Standards compliance, e.g. sod IE. Note that - // with some judicious character escaping (since certain - // types of escaping doesn't work) this is theoretically - // OK as long as innerHTML/cssText is not called. - // We believe that international is a reasonable default - // (that we will implement now), and once we do more - // extensive research, we may feel comfortable with dropping - // it down to edgy. - - // Edgy: alphanumeric, spaces, dashes, underscores and Unicode. Use of - // str(c)spn assumes that the string was already well formed - // Unicode (which of course it is). - if (strspn($font, $this->mask) !== strlen($font)) { - continue; - } - - // Historical: - // In the absence of innerHTML/cssText, these ugly - // transforms don't pose a security risk (as \\ and \" - // might--these escapes are not supported by most browsers). - // We could try to be clever and use single-quote wrapping - // when there is a double quote present, but I have choosen - // not to implement that. (NOTE: you can reduce the amount - // of escapes by one depending on what quoting style you use) - // $font = str_replace('\\', '\\5C ', $font); - // $font = str_replace('"', '\\22 ', $font); - // $font = str_replace("'", '\\27 ', $font); - - // font possibly with spaces, requires quoting - $final .= "'$font', "; - } - $final = rtrim($final, ', '); - if ($final === '') { - return false; - } - return $final; - } - -} - -// vim: et sw=4 sts=4 +mask = '_- '; + for ($c = 'a'; $c <= 'z'; $c++) { + $this->mask .= $c; + } + for ($c = 'A'; $c <= 'Z'; $c++) { + $this->mask .= $c; + } + for ($c = '0'; $c <= '9'; $c++) { + $this->mask .= $c; + } // cast-y, but should be fine + // special bytes used by UTF-8 + for ($i = 0x80; $i <= 0xFF; $i++) { + // We don't bother excluding invalid bytes in this range, + // because the our restriction of well-formed UTF-8 will + // prevent these from ever occurring. + $this->mask .= chr($i); + } + + /* + PHP's internal strcspn implementation is + O(length of string * length of mask), making it inefficient + for large masks. However, it's still faster than + preg_match 8) + for (p = s1;;) { + spanp = s2; + do { + if (*spanp == c || p == s1_end) { + return p - s1; + } + } while (spanp++ < (s2_end - 1)); + c = *++p; + } + */ + // possible optimization: invert the mask. + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + static $generic_names = array( + 'serif' => true, + 'sans-serif' => true, + 'monospace' => true, + 'fantasy' => true, + 'cursive' => true + ); + $allowed_fonts = $config->get('CSS.AllowedFonts'); + + // assume that no font names contain commas in them + $fonts = explode(',', $string); + $final = ''; + foreach ($fonts as $font) { + $font = trim($font); + if ($font === '') { + continue; + } + // match a generic name + if (isset($generic_names[$font])) { + if ($allowed_fonts === null || isset($allowed_fonts[$font])) { + $final .= $font . ', '; + } + continue; + } + // match a quoted name + if ($font[0] === '"' || $font[0] === "'") { + $length = strlen($font); + if ($length <= 2) { + continue; + } + $quote = $font[0]; + if ($font[$length - 1] !== $quote) { + continue; + } + $font = substr($font, 1, $length - 2); + } + + $font = $this->expandCSSEscape($font); + + // $font is a pure representation of the font name + + if ($allowed_fonts !== null && !isset($allowed_fonts[$font])) { + continue; + } + + if (ctype_alnum($font) && $font !== '') { + // very simple font, allow it in unharmed + $final .= $font . ', '; + continue; + } + + // bugger out on whitespace. form feed (0C) really + // shouldn't show up regardless + $font = str_replace(array("\n", "\t", "\r", "\x0C"), ' ', $font); + + // Here, there are various classes of characters which need + // to be treated differently: + // - Alphanumeric characters are essentially safe. We + // handled these above. + // - Spaces require quoting, though most parsers will do + // the right thing if there aren't any characters that + // can be misinterpreted + // - Dashes rarely occur, but they fairly unproblematic + // for parsing/rendering purposes. + // The above characters cover the majority of Western font + // names. + // - Arbitrary Unicode characters not in ASCII. Because + // most parsers give little thought to Unicode, treatment + // of these codepoints is basically uniform, even for + // punctuation-like codepoints. These characters can + // show up in non-Western pages and are supported by most + // major browsers, for example: "MS 明朝" is a + // legitimate font-name + // . See + // the CSS3 spec for more examples: + // + // You can see live samples of these on the Internet: + // + // However, most of these fonts have ASCII equivalents: + // for example, 'MS Mincho', and it's considered + // professional to use ASCII font names instead of + // Unicode font names. Thanks Takeshi Terada for + // providing this information. + // The following characters, to my knowledge, have not been + // used to name font names. + // - Single quote. While theoretically you might find a + // font name that has a single quote in its name (serving + // as an apostrophe, e.g. Dave's Scribble), I haven't + // been able to find any actual examples of this. + // Internet Explorer's cssText translation (which I + // believe is invoked by innerHTML) normalizes any + // quoting to single quotes, and fails to escape single + // quotes. (Note that this is not IE's behavior for all + // CSS properties, just some sort of special casing for + // font-family). So a single quote *cannot* be used + // safely in the font-family context if there will be an + // innerHTML/cssText translation. Note that Firefox 3.x + // does this too. + // - Double quote. In IE, these get normalized to + // single-quotes, no matter what the encoding. (Fun + // fact, in IE8, the 'content' CSS property gained + // support, where they special cased to preserve encoded + // double quotes, but still translate unadorned double + // quotes into single quotes.) So, because their + // fixpoint behavior is identical to single quotes, they + // cannot be allowed either. Firefox 3.x displays + // single-quote style behavior. + // - Backslashes are reduced by one (so \\ -> \) every + // iteration, so they cannot be used safely. This shows + // up in IE7, IE8 and FF3 + // - Semicolons, commas and backticks are handled properly. + // - The rest of the ASCII punctuation is handled properly. + // We haven't checked what browsers do to unadorned + // versions, but this is not important as long as the + // browser doesn't /remove/ surrounding quotes (as IE does + // for HTML). + // + // With these results in hand, we conclude that there are + // various levels of safety: + // - Paranoid: alphanumeric, spaces and dashes(?) + // - International: Paranoid + non-ASCII Unicode + // - Edgy: Everything except quotes, backslashes + // - NoJS: Standards compliance, e.g. sod IE. Note that + // with some judicious character escaping (since certain + // types of escaping doesn't work) this is theoretically + // OK as long as innerHTML/cssText is not called. + // We believe that international is a reasonable default + // (that we will implement now), and once we do more + // extensive research, we may feel comfortable with dropping + // it down to edgy. + + // Edgy: alphanumeric, spaces, dashes, underscores and Unicode. Use of + // str(c)spn assumes that the string was already well formed + // Unicode (which of course it is). + if (strspn($font, $this->mask) !== strlen($font)) { + continue; + } + + // Historical: + // In the absence of innerHTML/cssText, these ugly + // transforms don't pose a security risk (as \\ and \" + // might--these escapes are not supported by most browsers). + // We could try to be clever and use single-quote wrapping + // when there is a double quote present, but I have choosen + // not to implement that. (NOTE: you can reduce the amount + // of escapes by one depending on what quoting style you use) + // $font = str_replace('\\', '\\5C ', $font); + // $font = str_replace('"', '\\22 ', $font); + // $font = str_replace("'", '\\27 ', $font); + + // font possibly with spaces, requires quoting + $final .= "'$font', "; + } + $final = rtrim($final, ', '); + if ($final === '') { + return false; + } + return $final; + } + +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/CSS/Ident.php b/lib/purifier/HTMLPurifier/AttrDef/CSS/Ident.php index 5f13edfd10..973002c17f 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/CSS/Ident.php +++ b/lib/purifier/HTMLPurifier/AttrDef/CSS/Ident.php @@ -1,32 +1,32 @@ -def = $def; - $this->allow = $allow; - } - - /** - * Intercepts and removes !important if necessary - * @param string $string - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool|string - */ - public function validate($string, $config, $context) - { - // test for ! and important tokens - $string = trim($string); - $is_important = false; - // :TODO: optimization: test directly for !important and ! important - if (strlen($string) >= 9 && substr($string, -9) === 'important') { - $temp = rtrim(substr($string, 0, -9)); - // use a temp, because we might want to restore important - if (strlen($temp) >= 1 && substr($temp, -1) === '!') { - $string = rtrim(substr($temp, 0, -1)); - $is_important = true; - } - } - $string = $this->def->validate($string, $config, $context); - if ($this->allow && $is_important) { - $string .= ' !important'; - } - return $string; - } -} - -// vim: et sw=4 sts=4 +def = $def; + $this->allow = $allow; + } + + /** + * Intercepts and removes !important if necessary + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + // test for ! and important tokens + $string = trim($string); + $is_important = false; + // :TODO: optimization: test directly for !important and ! important + if (strlen($string) >= 9 && substr($string, -9) === 'important') { + $temp = rtrim(substr($string, 0, -9)); + // use a temp, because we might want to restore important + if (strlen($temp) >= 1 && substr($temp, -1) === '!') { + $string = rtrim(substr($temp, 0, -1)); + $is_important = true; + } + } + $string = $this->def->validate($string, $config, $context); + if ($this->allow && $is_important) { + $string .= ' !important'; + } + return $string; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/CSS/Length.php b/lib/purifier/HTMLPurifier/AttrDef/CSS/Length.php index 88da41d93d..f12453a04a 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/CSS/Length.php +++ b/lib/purifier/HTMLPurifier/AttrDef/CSS/Length.php @@ -1,77 +1,77 @@ -min = $min !== null ? HTMLPurifier_Length::make($min) : null; - $this->max = $max !== null ? HTMLPurifier_Length::make($max) : null; - } - - /** - * @param string $string - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool|string - */ - public function validate($string, $config, $context) - { - $string = $this->parseCDATA($string); - - // Optimizations - if ($string === '') { - return false; - } - if ($string === '0') { - return '0'; - } - if (strlen($string) === 1) { - return false; - } - - $length = HTMLPurifier_Length::make($string); - if (!$length->isValid()) { - return false; - } - - if ($this->min) { - $c = $length->compareTo($this->min); - if ($c === false) { - return false; - } - if ($c < 0) { - return false; - } - } - if ($this->max) { - $c = $length->compareTo($this->max); - if ($c === false) { - return false; - } - if ($c > 0) { - return false; - } - } - return $length->toString(); - } -} - -// vim: et sw=4 sts=4 +min = $min !== null ? HTMLPurifier_Length::make($min) : null; + $this->max = $max !== null ? HTMLPurifier_Length::make($max) : null; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $string = $this->parseCDATA($string); + + // Optimizations + if ($string === '') { + return false; + } + if ($string === '0') { + return '0'; + } + if (strlen($string) === 1) { + return false; + } + + $length = HTMLPurifier_Length::make($string); + if (!$length->isValid()) { + return false; + } + + if ($this->min) { + $c = $length->compareTo($this->min); + if ($c === false) { + return false; + } + if ($c < 0) { + return false; + } + } + if ($this->max) { + $c = $length->compareTo($this->max); + if ($c === false) { + return false; + } + if ($c > 0) { + return false; + } + } + return $length->toString(); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/CSS/ListStyle.php b/lib/purifier/HTMLPurifier/AttrDef/CSS/ListStyle.php index b4cce9a9df..e74d42654e 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/CSS/ListStyle.php +++ b/lib/purifier/HTMLPurifier/AttrDef/CSS/ListStyle.php @@ -1,112 +1,112 @@ -getCSSDefinition(); - $this->info['list-style-type'] = $def->info['list-style-type']; - $this->info['list-style-position'] = $def->info['list-style-position']; - $this->info['list-style-image'] = $def->info['list-style-image']; - } - - /** - * @param string $string - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool|string - */ - public function validate($string, $config, $context) - { - // regular pre-processing - $string = $this->parseCDATA($string); - if ($string === '') { - return false; - } - - // assumes URI doesn't have spaces in it - $bits = explode(' ', strtolower($string)); // bits to process - - $caught = array(); - $caught['type'] = false; - $caught['position'] = false; - $caught['image'] = false; - - $i = 0; // number of catches - $none = false; - - foreach ($bits as $bit) { - if ($i >= 3) { - return; - } // optimization bit - if ($bit === '') { - continue; - } - foreach ($caught as $key => $status) { - if ($status !== false) { - continue; - } - $r = $this->info['list-style-' . $key]->validate($bit, $config, $context); - if ($r === false) { - continue; - } - if ($r === 'none') { - if ($none) { - continue; - } else { - $none = true; - } - if ($key == 'image') { - continue; - } - } - $caught[$key] = $r; - $i++; - break; - } - } - - if (!$i) { - return false; - } - - $ret = array(); - - // construct type - if ($caught['type']) { - $ret[] = $caught['type']; - } - - // construct image - if ($caught['image']) { - $ret[] = $caught['image']; - } - - // construct position - if ($caught['position']) { - $ret[] = $caught['position']; - } - - if (empty($ret)) { - return false; - } - return implode(' ', $ret); - } -} - -// vim: et sw=4 sts=4 +getCSSDefinition(); + $this->info['list-style-type'] = $def->info['list-style-type']; + $this->info['list-style-position'] = $def->info['list-style-position']; + $this->info['list-style-image'] = $def->info['list-style-image']; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + // regular pre-processing + $string = $this->parseCDATA($string); + if ($string === '') { + return false; + } + + // assumes URI doesn't have spaces in it + $bits = explode(' ', strtolower($string)); // bits to process + + $caught = array(); + $caught['type'] = false; + $caught['position'] = false; + $caught['image'] = false; + + $i = 0; // number of catches + $none = false; + + foreach ($bits as $bit) { + if ($i >= 3) { + return; + } // optimization bit + if ($bit === '') { + continue; + } + foreach ($caught as $key => $status) { + if ($status !== false) { + continue; + } + $r = $this->info['list-style-' . $key]->validate($bit, $config, $context); + if ($r === false) { + continue; + } + if ($r === 'none') { + if ($none) { + continue; + } else { + $none = true; + } + if ($key == 'image') { + continue; + } + } + $caught[$key] = $r; + $i++; + break; + } + } + + if (!$i) { + return false; + } + + $ret = array(); + + // construct type + if ($caught['type']) { + $ret[] = $caught['type']; + } + + // construct image + if ($caught['image']) { + $ret[] = $caught['image']; + } + + // construct position + if ($caught['position']) { + $ret[] = $caught['position']; + } + + if (empty($ret)) { + return false; + } + return implode(' ', $ret); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/CSS/Multiple.php b/lib/purifier/HTMLPurifier/AttrDef/CSS/Multiple.php index 4efb6c04f1..e707f871ca 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/CSS/Multiple.php +++ b/lib/purifier/HTMLPurifier/AttrDef/CSS/Multiple.php @@ -1,71 +1,71 @@ -single = $single; - $this->max = $max; - } - - /** - * @param string $string - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool|string - */ - public function validate($string, $config, $context) - { - $string = $this->mungeRgb($this->parseCDATA($string)); - if ($string === '') { - return false; - } - $parts = explode(' ', $string); // parseCDATA replaced \r, \t and \n - $length = count($parts); - $final = ''; - for ($i = 0, $num = 0; $i < $length && $num < $this->max; $i++) { - if (ctype_space($parts[$i])) { - continue; - } - $result = $this->single->validate($parts[$i], $config, $context); - if ($result !== false) { - $final .= $result . ' '; - $num++; - } - } - if ($final === '') { - return false; - } - return rtrim($final); - } -} - -// vim: et sw=4 sts=4 +single = $single; + $this->max = $max; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $string = $this->mungeRgb($this->parseCDATA($string)); + if ($string === '') { + return false; + } + $parts = explode(' ', $string); // parseCDATA replaced \r, \t and \n + $length = count($parts); + $final = ''; + for ($i = 0, $num = 0; $i < $length && $num < $this->max; $i++) { + if (ctype_space($parts[$i])) { + continue; + } + $result = $this->single->validate($parts[$i], $config, $context); + if ($result !== false) { + $final .= $result . ' '; + $num++; + } + } + if ($final === '') { + return false; + } + return rtrim($final); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/CSS/Number.php b/lib/purifier/HTMLPurifier/AttrDef/CSS/Number.php index c78f6c9dfb..ef49d20fdd 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/CSS/Number.php +++ b/lib/purifier/HTMLPurifier/AttrDef/CSS/Number.php @@ -1,84 +1,90 @@ -non_negative = $non_negative; - } - - /** - * @param string $number - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return string|bool - * @warning Some contexts do not pass $config, $context. These - * variables should not be used without checking HTMLPurifier_Length - */ - public function validate($number, $config, $context) - { - $number = $this->parseCDATA($number); - - if ($number === '') { - return false; - } - if ($number === '0') { - return '0'; - } - - $sign = ''; - switch ($number[0]) { - case '-': - if ($this->non_negative) { - return false; - } - $sign = '-'; - case '+': - $number = substr($number, 1); - } - - if (ctype_digit($number)) { - $number = ltrim($number, '0'); - return $number ? $sign . $number : '0'; - } - - // Period is the only non-numeric character allowed - if (strpos($number, '.') === false) { - return false; - } - - list($left, $right) = explode('.', $number, 2); - - if ($left === '' && $right === '') { - return false; - } - if ($left !== '' && !ctype_digit($left)) { - return false; - } - - $left = ltrim($left, '0'); - $right = rtrim($right, '0'); - - if ($right === '') { - return $left ? $sign . $left : '0'; - } elseif (!ctype_digit($right)) { - return false; - } - return $sign . $left . '.' . $right; - } -} - -// vim: et sw=4 sts=4 +non_negative = $non_negative; + } + + /** + * @param string $number + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string|bool + * @warning Some contexts do not pass $config, $context. These + * variables should not be used without checking HTMLPurifier_Length + */ + public function validate($number, $config, $context) + { + $number = $this->parseCDATA($number); + + if ($number === '') { + return false; + } + if ($number === '0') { + return '0'; + } + + $sign = ''; + switch ($number[0]) { + case '-': + if ($this->non_negative) { + return false; + } + $sign = '-'; + case '+': + $number = substr($number, 1); + } + + if (ctype_digit($number)) { + $number = ltrim($number, '0'); + return $number ? $sign . $number : '0'; + } + + // Period is the only non-numeric character allowed + if (strpos($number, '.') === false) { + return false; + } + + list($left, $right) = explode('.', $number, 2); + + if ($left === '' && $right === '') { + return false; + } + if ($left !== '' && !ctype_digit($left)) { + return false; + } + + // Remove leading zeros until positive number or a zero stays left + if (ltrim($left, '0') != '') { + $left = ltrim($left, '0'); + } else { + $left = '0'; + } + + $right = rtrim($right, '0'); + + if ($right === '') { + return $left ? $sign . $left : '0'; + } elseif (!ctype_digit($right)) { + return false; + } + return $sign . $left . '.' . $right; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/CSS/Percentage.php b/lib/purifier/HTMLPurifier/AttrDef/CSS/Percentage.php index aac1a6f5fc..f0f25c50a8 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/CSS/Percentage.php +++ b/lib/purifier/HTMLPurifier/AttrDef/CSS/Percentage.php @@ -1,54 +1,54 @@ -number_def = new HTMLPurifier_AttrDef_CSS_Number($non_negative); - } - - /** - * @param string $string - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool|string - */ - public function validate($string, $config, $context) - { - $string = $this->parseCDATA($string); - - if ($string === '') { - return false; - } - $length = strlen($string); - if ($length === 1) { - return false; - } - if ($string[$length - 1] !== '%') { - return false; - } - - $number = substr($string, 0, $length - 1); - $number = $this->number_def->validate($number, $config, $context); - - if ($number === false) { - return false; - } - return "$number%"; - } -} - -// vim: et sw=4 sts=4 +number_def = new HTMLPurifier_AttrDef_CSS_Number($non_negative); + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $string = $this->parseCDATA($string); + + if ($string === '') { + return false; + } + $length = strlen($string); + if ($length === 1) { + return false; + } + if ($string[$length - 1] !== '%') { + return false; + } + + $number = substr($string, 0, $length - 1); + $number = $this->number_def->validate($number, $config, $context); + + if ($number === false) { + return false; + } + return "$number%"; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/CSS/TextDecoration.php b/lib/purifier/HTMLPurifier/AttrDef/CSS/TextDecoration.php index 3992de0e60..5fd4b7f7b4 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/CSS/TextDecoration.php +++ b/lib/purifier/HTMLPurifier/AttrDef/CSS/TextDecoration.php @@ -1,46 +1,46 @@ - true, - 'overline' => true, - 'underline' => true, - ); - - $string = strtolower($this->parseCDATA($string)); - - if ($string === 'none') { - return $string; - } - - $parts = explode(' ', $string); - $final = ''; - foreach ($parts as $part) { - if (isset($allowed_values[$part])) { - $final .= $part . ' '; - } - } - $final = rtrim($final); - if ($final === '') { - return false; - } - return $final; - } -} - -// vim: et sw=4 sts=4 + true, + 'overline' => true, + 'underline' => true, + ); + + $string = strtolower($this->parseCDATA($string)); + + if ($string === 'none') { + return $string; + } + + $parts = explode(' ', $string); + $final = ''; + foreach ($parts as $part) { + if (isset($allowed_values[$part])) { + $final .= $part . ' '; + } + } + $final = rtrim($final); + if ($final === '') { + return false; + } + return $final; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/CSS/URI.php b/lib/purifier/HTMLPurifier/AttrDef/CSS/URI.php index 3d18b3289e..6617acace5 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/CSS/URI.php +++ b/lib/purifier/HTMLPurifier/AttrDef/CSS/URI.php @@ -1,77 +1,77 @@ -parseCDATA($uri_string); - if (strpos($uri_string, 'url(') !== 0) { - return false; - } - $uri_string = substr($uri_string, 4); - if (strlen($uri_string) == 0) { - return false; - } - $new_length = strlen($uri_string) - 1; - if ($uri_string[$new_length] != ')') { - return false; - } - $uri = trim(substr($uri_string, 0, $new_length)); - - if (!empty($uri) && ($uri[0] == "'" || $uri[0] == '"')) { - $quote = $uri[0]; - $new_length = strlen($uri) - 1; - if ($uri[$new_length] !== $quote) { - return false; - } - $uri = substr($uri, 1, $new_length - 1); - } - - $uri = $this->expandCSSEscape($uri); - - $result = parent::validate($uri, $config, $context); - - if ($result === false) { - return false; - } - - // extra sanity check; should have been done by URI - $result = str_replace(array('"', "\\", "\n", "\x0c", "\r"), "", $result); - - // suspicious characters are ()'; we're going to percent encode - // them for safety. - $result = str_replace(array('(', ')', "'"), array('%28', '%29', '%27'), $result); - - // there's an extra bug where ampersands lose their escaping on - // an innerHTML cycle, so a very unlucky query parameter could - // then change the meaning of the URL. Unfortunately, there's - // not much we can do about that... - return "url(\"$result\")"; - } -} - -// vim: et sw=4 sts=4 +parseCDATA($uri_string); + if (strpos($uri_string, 'url(') !== 0) { + return false; + } + $uri_string = substr($uri_string, 4); + if (strlen($uri_string) == 0) { + return false; + } + $new_length = strlen($uri_string) - 1; + if ($uri_string[$new_length] != ')') { + return false; + } + $uri = trim(substr($uri_string, 0, $new_length)); + + if (!empty($uri) && ($uri[0] == "'" || $uri[0] == '"')) { + $quote = $uri[0]; + $new_length = strlen($uri) - 1; + if ($uri[$new_length] !== $quote) { + return false; + } + $uri = substr($uri, 1, $new_length - 1); + } + + $uri = $this->expandCSSEscape($uri); + + $result = parent::validate($uri, $config, $context); + + if ($result === false) { + return false; + } + + // extra sanity check; should have been done by URI + $result = str_replace(array('"', "\\", "\n", "\x0c", "\r"), "", $result); + + // suspicious characters are ()'; we're going to percent encode + // them for safety. + $result = str_replace(array('(', ')', "'"), array('%28', '%29', '%27'), $result); + + // there's an extra bug where ampersands lose their escaping on + // an innerHTML cycle, so a very unlucky query parameter could + // then change the meaning of the URL. Unfortunately, there's + // not much we can do about that... + return "url(\"$result\")"; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/Clone.php b/lib/purifier/HTMLPurifier/AttrDef/Clone.php index b181d1bcd4..6698a00c01 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/Clone.php +++ b/lib/purifier/HTMLPurifier/AttrDef/Clone.php @@ -1,44 +1,44 @@ -clone = $clone; - } - - /** - * @param string $v - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool|string - */ - public function validate($v, $config, $context) - { - return $this->clone->validate($v, $config, $context); - } - - /** - * @param string $string - * @return HTMLPurifier_AttrDef - */ - public function make($string) - { - return clone $this->clone; - } -} - -// vim: et sw=4 sts=4 +clone = $clone; + } + + /** + * @param string $v + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($v, $config, $context) + { + return $this->clone->validate($v, $config, $context); + } + + /** + * @param string $string + * @return HTMLPurifier_AttrDef + */ + public function make($string) + { + return clone $this->clone; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/Enum.php b/lib/purifier/HTMLPurifier/AttrDef/Enum.php index b40122b6ca..8abda7f6e2 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/Enum.php +++ b/lib/purifier/HTMLPurifier/AttrDef/Enum.php @@ -1,73 +1,73 @@ -valid_values = array_flip($valid_values); - $this->case_sensitive = $case_sensitive; - } - - /** - * @param string $string - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool|string - */ - public function validate($string, $config, $context) - { - $string = trim($string); - if (!$this->case_sensitive) { - // we may want to do full case-insensitive libraries - $string = ctype_lower($string) ? $string : strtolower($string); - } - $result = isset($this->valid_values[$string]); - - return $result ? $string : false; - } - - /** - * @param string $string In form of comma-delimited list of case-insensitive - * valid values. Example: "foo,bar,baz". Prepend "s:" to make - * case sensitive - * @return HTMLPurifier_AttrDef_Enum - */ - public function make($string) - { - if (strlen($string) > 2 && $string[0] == 's' && $string[1] == ':') { - $string = substr($string, 2); - $sensitive = true; - } else { - $sensitive = false; - } - $values = explode(',', $string); - return new HTMLPurifier_AttrDef_Enum($values, $sensitive); - } -} - -// vim: et sw=4 sts=4 +valid_values = array_flip($valid_values); + $this->case_sensitive = $case_sensitive; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $string = trim($string); + if (!$this->case_sensitive) { + // we may want to do full case-insensitive libraries + $string = ctype_lower($string) ? $string : strtolower($string); + } + $result = isset($this->valid_values[$string]); + + return $result ? $string : false; + } + + /** + * @param string $string In form of comma-delimited list of case-insensitive + * valid values. Example: "foo,bar,baz". Prepend "s:" to make + * case sensitive + * @return HTMLPurifier_AttrDef_Enum + */ + public function make($string) + { + if (strlen($string) > 2 && $string[0] == 's' && $string[1] == ':') { + $string = substr($string, 2); + $sensitive = true; + } else { + $sensitive = false; + } + $values = explode(',', $string); + return new HTMLPurifier_AttrDef_Enum($values, $sensitive); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/HTML/Bool.php b/lib/purifier/HTMLPurifier/AttrDef/HTML/Bool.php index 953a36a6c9..be3bbc8dc0 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/HTML/Bool.php +++ b/lib/purifier/HTMLPurifier/AttrDef/HTML/Bool.php @@ -1,48 +1,48 @@ -name = $name; - } - - /** - * @param string $string - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool|string - */ - public function validate($string, $config, $context) - { - return $this->name; - } - - /** - * @param string $string Name of attribute - * @return HTMLPurifier_AttrDef_HTML_Bool - */ - public function make($string) - { - return new HTMLPurifier_AttrDef_HTML_Bool($string); - } -} - -// vim: et sw=4 sts=4 +name = $name; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + return $this->name; + } + + /** + * @param string $string Name of attribute + * @return HTMLPurifier_AttrDef_HTML_Bool + */ + public function make($string) + { + return new HTMLPurifier_AttrDef_HTML_Bool($string); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/HTML/Class.php b/lib/purifier/HTMLPurifier/AttrDef/HTML/Class.php index b874c7e1ae..d5013488fc 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/HTML/Class.php +++ b/lib/purifier/HTMLPurifier/AttrDef/HTML/Class.php @@ -1,48 +1,48 @@ -getDefinition('HTML')->doctype->name; - if ($name == "XHTML 1.1" || $name == "XHTML 2.0") { - return parent::split($string, $config, $context); - } else { - return preg_split('/\s+/', $string); - } - } - - /** - * @param array $tokens - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return array - */ - protected function filter($tokens, $config, $context) - { - $allowed = $config->get('Attr.AllowedClasses'); - $forbidden = $config->get('Attr.ForbiddenClasses'); - $ret = array(); - foreach ($tokens as $token) { - if (($allowed === null || isset($allowed[$token])) && - !isset($forbidden[$token]) && - // We need this O(n) check because of PHP's array - // implementation that casts -0 to 0. - !in_array($token, $ret, true) - ) { - $ret[] = $token; - } - } - return $ret; - } -} +getDefinition('HTML')->doctype->name; + if ($name == "XHTML 1.1" || $name == "XHTML 2.0") { + return parent::split($string, $config, $context); + } else { + return preg_split('/\s+/', $string); + } + } + + /** + * @param array $tokens + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + protected function filter($tokens, $config, $context) + { + $allowed = $config->get('Attr.AllowedClasses'); + $forbidden = $config->get('Attr.ForbiddenClasses'); + $ret = array(); + foreach ($tokens as $token) { + if (($allowed === null || isset($allowed[$token])) && + !isset($forbidden[$token]) && + // We need this O(n) check because of PHP's array + // implementation that casts -0 to 0. + !in_array($token, $ret, true) + ) { + $ret[] = $token; + } + } + return $ret; + } +} diff --git a/lib/purifier/HTMLPurifier/AttrDef/HTML/Color.php b/lib/purifier/HTMLPurifier/AttrDef/HTML/Color.php index 25c93fc6e5..946ebb7820 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/HTML/Color.php +++ b/lib/purifier/HTMLPurifier/AttrDef/HTML/Color.php @@ -1,51 +1,51 @@ -get('Core.ColorKeywords'); - } - - $string = trim($string); - - if (empty($string)) { - return false; - } - $lower = strtolower($string); - if (isset($colors[$lower])) { - return $colors[$lower]; - } - if ($string[0] === '#') { - $hex = substr($string, 1); - } else { - $hex = $string; - } - - $length = strlen($hex); - if ($length !== 3 && $length !== 6) { - return false; - } - if (!ctype_xdigit($hex)) { - return false; - } - if ($length === 3) { - $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2]; - } - return "#$hex"; - } -} - -// vim: et sw=4 sts=4 +get('Core.ColorKeywords'); + } + + $string = trim($string); + + if (empty($string)) { + return false; + } + $lower = strtolower($string); + if (isset($colors[$lower])) { + return $colors[$lower]; + } + if ($string[0] === '#') { + $hex = substr($string, 1); + } else { + $hex = $string; + } + + $length = strlen($hex); + if ($length !== 3 && $length !== 6) { + return false; + } + if (!ctype_xdigit($hex)) { + return false; + } + if ($length === 3) { + $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2]; + } + return "#$hex"; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/HTML/FrameTarget.php b/lib/purifier/HTMLPurifier/AttrDef/HTML/FrameTarget.php index 7446b6da99..d79ba12b3f 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/HTML/FrameTarget.php +++ b/lib/purifier/HTMLPurifier/AttrDef/HTML/FrameTarget.php @@ -1,38 +1,38 @@ -valid_values === false) { - $this->valid_values = $config->get('Attr.AllowedFrameTargets'); - } - return parent::validate($string, $config, $context); - } -} - -// vim: et sw=4 sts=4 +valid_values === false) { + $this->valid_values = $config->get('Attr.AllowedFrameTargets'); + } + return parent::validate($string, $config, $context); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/HTML/ID.php b/lib/purifier/HTMLPurifier/AttrDef/HTML/ID.php index 7e464ba546..4ba45610fe 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/HTML/ID.php +++ b/lib/purifier/HTMLPurifier/AttrDef/HTML/ID.php @@ -1,113 +1,113 @@ -selector = $selector; - } - - /** - * @param string $id - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool|string - */ - public function validate($id, $config, $context) - { - if (!$this->selector && !$config->get('Attr.EnableID')) { - return false; - } - - $id = trim($id); // trim it first - - if ($id === '') { - return false; - } - - $prefix = $config->get('Attr.IDPrefix'); - if ($prefix !== '') { - $prefix .= $config->get('Attr.IDPrefixLocal'); - // prevent re-appending the prefix - if (strpos($id, $prefix) !== 0) { - $id = $prefix . $id; - } - } elseif ($config->get('Attr.IDPrefixLocal') !== '') { - trigger_error( - '%Attr.IDPrefixLocal cannot be used unless ' . - '%Attr.IDPrefix is set', - E_USER_WARNING - ); - } - - if (!$this->selector) { - $id_accumulator =& $context->get('IDAccumulator'); - if (isset($id_accumulator->ids[$id])) { - return false; - } - } - - // we purposely avoid using regex, hopefully this is faster - - if ($config->get('Attr.ID.HTML5') === true) { - if (preg_match('/[\t\n\x0b\x0c ]/', $id)) { - return false; - } - } else { - if (ctype_alpha($id)) { - // OK - } else { - if (!ctype_alpha(@$id[0])) { - return false; - } - // primitive style of regexps, I suppose - $trim = trim( - $id, - 'A..Za..z0..9:-._' - ); - if ($trim !== '') { - return false; - } - } - } - - $regexp = $config->get('Attr.IDBlacklistRegexp'); - if ($regexp && preg_match($regexp, $id)) { - return false; - } - - if (!$this->selector) { - $id_accumulator->add($id); - } - - // if no change was made to the ID, return the result - // else, return the new id if stripping whitespace made it - // valid, or return false. - return $id; - } -} - -// vim: et sw=4 sts=4 +selector = $selector; + } + + /** + * @param string $id + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($id, $config, $context) + { + if (!$this->selector && !$config->get('Attr.EnableID')) { + return false; + } + + $id = trim($id); // trim it first + + if ($id === '') { + return false; + } + + $prefix = $config->get('Attr.IDPrefix'); + if ($prefix !== '') { + $prefix .= $config->get('Attr.IDPrefixLocal'); + // prevent re-appending the prefix + if (strpos($id, $prefix) !== 0) { + $id = $prefix . $id; + } + } elseif ($config->get('Attr.IDPrefixLocal') !== '') { + trigger_error( + '%Attr.IDPrefixLocal cannot be used unless ' . + '%Attr.IDPrefix is set', + E_USER_WARNING + ); + } + + if (!$this->selector) { + $id_accumulator =& $context->get('IDAccumulator'); + if (isset($id_accumulator->ids[$id])) { + return false; + } + } + + // we purposely avoid using regex, hopefully this is faster + + if ($config->get('Attr.ID.HTML5') === true) { + if (preg_match('/[\t\n\x0b\x0c ]/', $id)) { + return false; + } + } else { + if (ctype_alpha($id)) { + // OK + } else { + if (!ctype_alpha(@$id[0])) { + return false; + } + // primitive style of regexps, I suppose + $trim = trim( + $id, + 'A..Za..z0..9:-._' + ); + if ($trim !== '') { + return false; + } + } + } + + $regexp = $config->get('Attr.IDBlacklistRegexp'); + if ($regexp && preg_match($regexp, $id)) { + return false; + } + + if (!$this->selector) { + $id_accumulator->add($id); + } + + // if no change was made to the ID, return the result + // else, return the new id if stripping whitespace made it + // valid, or return false. + return $id; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/HTML/Length.php b/lib/purifier/HTMLPurifier/AttrDef/HTML/Length.php index c8f5188656..1c4006fbbd 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/HTML/Length.php +++ b/lib/purifier/HTMLPurifier/AttrDef/HTML/Length.php @@ -1,56 +1,56 @@ - 100) { - return '100%'; - } - return ((string)$points) . '%'; - } -} - -// vim: et sw=4 sts=4 + 100) { + return '100%'; + } + return ((string)$points) . '%'; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/HTML/LinkTypes.php b/lib/purifier/HTMLPurifier/AttrDef/HTML/LinkTypes.php index 3f56934ffc..63fa04c15c 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/HTML/LinkTypes.php +++ b/lib/purifier/HTMLPurifier/AttrDef/HTML/LinkTypes.php @@ -1,72 +1,72 @@ - 'AllowedRel', - 'rev' => 'AllowedRev' - ); - if (!isset($configLookup[$name])) { - trigger_error( - 'Unrecognized attribute name for link ' . - 'relationship.', - E_USER_ERROR - ); - return; - } - $this->name = $configLookup[$name]; - } - - /** - * @param string $string - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool|string - */ - public function validate($string, $config, $context) - { - $allowed = $config->get('Attr.' . $this->name); - if (empty($allowed)) { - return false; - } - - $string = $this->parseCDATA($string); - $parts = explode(' ', $string); - - // lookup to prevent duplicates - $ret_lookup = array(); - foreach ($parts as $part) { - $part = strtolower(trim($part)); - if (!isset($allowed[$part])) { - continue; - } - $ret_lookup[$part] = true; - } - - if (empty($ret_lookup)) { - return false; - } - $string = implode(' ', array_keys($ret_lookup)); - return $string; - } -} - -// vim: et sw=4 sts=4 + 'AllowedRel', + 'rev' => 'AllowedRev' + ); + if (!isset($configLookup[$name])) { + trigger_error( + 'Unrecognized attribute name for link ' . + 'relationship.', + E_USER_ERROR + ); + return; + } + $this->name = $configLookup[$name]; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $allowed = $config->get('Attr.' . $this->name); + if (empty($allowed)) { + return false; + } + + $string = $this->parseCDATA($string); + $parts = explode(' ', $string); + + // lookup to prevent duplicates + $ret_lookup = array(); + foreach ($parts as $part) { + $part = strtolower(trim($part)); + if (!isset($allowed[$part])) { + continue; + } + $ret_lookup[$part] = true; + } + + if (empty($ret_lookup)) { + return false; + } + $string = implode(' ', array_keys($ret_lookup)); + return $string; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/HTML/MultiLength.php b/lib/purifier/HTMLPurifier/AttrDef/HTML/MultiLength.php index eb713e15b9..bbb20f2f80 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/HTML/MultiLength.php +++ b/lib/purifier/HTMLPurifier/AttrDef/HTML/MultiLength.php @@ -1,60 +1,60 @@ -split($string, $config, $context); - $tokens = $this->filter($tokens, $config, $context); - if (empty($tokens)) { - return false; - } - return implode(' ', $tokens); - } - - /** - * Splits a space separated list of tokens into its constituent parts. - * @param string $string - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return array - */ - protected function split($string, $config, $context) - { - // OPTIMIZABLE! - // do the preg_match, capture all subpatterns for reformulation - - // we don't support U+00A1 and up codepoints or - // escaping because I don't know how to do that with regexps - // and plus it would complicate optimization efforts (you never - // see that anyway). - $pattern = '/(?:(?<=\s)|\A)' . // look behind for space or string start - '((?:--|-?[A-Za-z_])[A-Za-z_\-0-9]*)' . - '(?:(?=\s)|\z)/'; // look ahead for space or string end - preg_match_all($pattern, $string, $matches); - return $matches[1]; - } - - /** - * Template method for removing certain tokens based on arbitrary criteria. - * @note If we wanted to be really functional, we'd do an array_filter - * with a callback. But... we're not. - * @param array $tokens - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return array - */ - protected function filter($tokens, $config, $context) - { - return $tokens; - } -} - -// vim: et sw=4 sts=4 +split($string, $config, $context); + $tokens = $this->filter($tokens, $config, $context); + if (empty($tokens)) { + return false; + } + return implode(' ', $tokens); + } + + /** + * Splits a space separated list of tokens into its constituent parts. + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + protected function split($string, $config, $context) + { + // OPTIMIZABLE! + // do the preg_match, capture all subpatterns for reformulation + + // we don't support U+00A1 and up codepoints or + // escaping because I don't know how to do that with regexps + // and plus it would complicate optimization efforts (you never + // see that anyway). + $pattern = '/(?:(?<=\s)|\A)' . // look behind for space or string start + '((?:--|-?[A-Za-z_])[A-Za-z_\-0-9]*)' . + '(?:(?=\s)|\z)/'; // look ahead for space or string end + preg_match_all($pattern, $string, $matches); + return $matches[1]; + } + + /** + * Template method for removing certain tokens based on arbitrary criteria. + * @note If we wanted to be really functional, we'd do an array_filter + * with a callback. But... we're not. + * @param array $tokens + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + protected function filter($tokens, $config, $context) + { + return $tokens; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/HTML/Pixels.php b/lib/purifier/HTMLPurifier/AttrDef/HTML/Pixels.php index 1a68f23855..a1d019e095 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/HTML/Pixels.php +++ b/lib/purifier/HTMLPurifier/AttrDef/HTML/Pixels.php @@ -1,76 +1,76 @@ -max = $max; - } - - /** - * @param string $string - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool|string - */ - public function validate($string, $config, $context) - { - $string = trim($string); - if ($string === '0') { - return $string; - } - if ($string === '') { - return false; - } - $length = strlen($string); - if (substr($string, $length - 2) == 'px') { - $string = substr($string, 0, $length - 2); - } - if (!is_numeric($string)) { - return false; - } - $int = (int)$string; - - if ($int < 0) { - return '0'; - } - - // upper-bound value, extremely high values can - // crash operating systems, see - // WARNING, above link WILL crash you if you're using Windows - - if ($this->max !== null && $int > $this->max) { - return (string)$this->max; - } - return (string)$int; - } - - /** - * @param string $string - * @return HTMLPurifier_AttrDef - */ - public function make($string) - { - if ($string === '') { - $max = null; - } else { - $max = (int)$string; - } - $class = get_class($this); - return new $class($max); - } -} - -// vim: et sw=4 sts=4 +max = $max; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $string = trim($string); + if ($string === '0') { + return $string; + } + if ($string === '') { + return false; + } + $length = strlen($string); + if (substr($string, $length - 2) == 'px') { + $string = substr($string, 0, $length - 2); + } + if (!is_numeric($string)) { + return false; + } + $int = (int)$string; + + if ($int < 0) { + return '0'; + } + + // upper-bound value, extremely high values can + // crash operating systems, see + // WARNING, above link WILL crash you if you're using Windows + + if ($this->max !== null && $int > $this->max) { + return (string)$this->max; + } + return (string)$int; + } + + /** + * @param string $string + * @return HTMLPurifier_AttrDef + */ + public function make($string) + { + if ($string === '') { + $max = null; + } else { + $max = (int)$string; + } + $class = get_class($this); + return new $class($max); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/Integer.php b/lib/purifier/HTMLPurifier/AttrDef/Integer.php index c98376d750..400e707d2f 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/Integer.php +++ b/lib/purifier/HTMLPurifier/AttrDef/Integer.php @@ -1,91 +1,91 @@ -negative = $negative; - $this->zero = $zero; - $this->positive = $positive; - } - - /** - * @param string $integer - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool|string - */ - public function validate($integer, $config, $context) - { - $integer = $this->parseCDATA($integer); - if ($integer === '') { - return false; - } - - // we could possibly simply typecast it to integer, but there are - // certain fringe cases that must not return an integer. - - // clip leading sign - if ($this->negative && $integer[0] === '-') { - $digits = substr($integer, 1); - if ($digits === '0') { - $integer = '0'; - } // rm minus sign for zero - } elseif ($this->positive && $integer[0] === '+') { - $digits = $integer = substr($integer, 1); // rm unnecessary plus - } else { - $digits = $integer; - } - - // test if it's numeric - if (!ctype_digit($digits)) { - return false; - } - - // perform scope tests - if (!$this->zero && $integer == 0) { - return false; - } - if (!$this->positive && $integer > 0) { - return false; - } - if (!$this->negative && $integer < 0) { - return false; - } - - return $integer; - } -} - -// vim: et sw=4 sts=4 +negative = $negative; + $this->zero = $zero; + $this->positive = $positive; + } + + /** + * @param string $integer + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($integer, $config, $context) + { + $integer = $this->parseCDATA($integer); + if ($integer === '') { + return false; + } + + // we could possibly simply typecast it to integer, but there are + // certain fringe cases that must not return an integer. + + // clip leading sign + if ($this->negative && $integer[0] === '-') { + $digits = substr($integer, 1); + if ($digits === '0') { + $integer = '0'; + } // rm minus sign for zero + } elseif ($this->positive && $integer[0] === '+') { + $digits = $integer = substr($integer, 1); // rm unnecessary plus + } else { + $digits = $integer; + } + + // test if it's numeric + if (!ctype_digit($digits)) { + return false; + } + + // perform scope tests + if (!$this->zero && $integer == 0) { + return false; + } + if (!$this->positive && $integer > 0) { + return false; + } + if (!$this->negative && $integer < 0) { + return false; + } + + return $integer; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/Lang.php b/lib/purifier/HTMLPurifier/AttrDef/Lang.php index 6ad0f799df..2a55cea642 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/Lang.php +++ b/lib/purifier/HTMLPurifier/AttrDef/Lang.php @@ -1,86 +1,86 @@ - 8 || !ctype_alnum($subtags[1])) { - return $new_string; - } - if (!ctype_lower($subtags[1])) { - $subtags[1] = strtolower($subtags[1]); - } - - $new_string .= '-' . $subtags[1]; - if ($num_subtags == 2) { - return $new_string; - } - - // process all other subtags, index 2 and up - for ($i = 2; $i < $num_subtags; $i++) { - $length = strlen($subtags[$i]); - if ($length == 0 || $length > 8 || !ctype_alnum($subtags[$i])) { - return $new_string; - } - if (!ctype_lower($subtags[$i])) { - $subtags[$i] = strtolower($subtags[$i]); - } - $new_string .= '-' . $subtags[$i]; - } - return $new_string; - } -} - -// vim: et sw=4 sts=4 + 8 || !ctype_alnum($subtags[1])) { + return $new_string; + } + if (!ctype_lower($subtags[1])) { + $subtags[1] = strtolower($subtags[1]); + } + + $new_string .= '-' . $subtags[1]; + if ($num_subtags == 2) { + return $new_string; + } + + // process all other subtags, index 2 and up + for ($i = 2; $i < $num_subtags; $i++) { + $length = strlen($subtags[$i]); + if ($length == 0 || $length > 8 || !ctype_alnum($subtags[$i])) { + return $new_string; + } + if (!ctype_lower($subtags[$i])) { + $subtags[$i] = strtolower($subtags[$i]); + } + $new_string .= '-' . $subtags[$i]; + } + return $new_string; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/Switch.php b/lib/purifier/HTMLPurifier/AttrDef/Switch.php index 078291f584..c7eb3199a4 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/Switch.php +++ b/lib/purifier/HTMLPurifier/AttrDef/Switch.php @@ -1,53 +1,53 @@ -tag = $tag; - $this->withTag = $with_tag; - $this->withoutTag = $without_tag; - } - - /** - * @param string $string - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool|string - */ - public function validate($string, $config, $context) - { - $token = $context->get('CurrentToken', true); - if (!$token || $token->name !== $this->tag) { - return $this->withoutTag->validate($string, $config, $context); - } else { - return $this->withTag->validate($string, $config, $context); - } - } -} - -// vim: et sw=4 sts=4 +tag = $tag; + $this->withTag = $with_tag; + $this->withoutTag = $without_tag; + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $token = $context->get('CurrentToken', true); + if (!$token || $token->name !== $this->tag) { + return $this->withoutTag->validate($string, $config, $context); + } else { + return $this->withTag->validate($string, $config, $context); + } + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/Text.php b/lib/purifier/HTMLPurifier/AttrDef/Text.php index 9f23bac4a2..4553a4ea9b 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/Text.php +++ b/lib/purifier/HTMLPurifier/AttrDef/Text.php @@ -1,21 +1,21 @@ -parseCDATA($string); - } -} - -// vim: et sw=4 sts=4 +parseCDATA($string); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/URI.php b/lib/purifier/HTMLPurifier/AttrDef/URI.php index a1097cd918..c1cd89772c 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/URI.php +++ b/lib/purifier/HTMLPurifier/AttrDef/URI.php @@ -1,111 +1,111 @@ -parser = new HTMLPurifier_URIParser(); - $this->embedsResource = (bool)$embeds_resource; - } - - /** - * @param string $string - * @return HTMLPurifier_AttrDef_URI - */ - public function make($string) - { - $embeds = ($string === 'embedded'); - return new HTMLPurifier_AttrDef_URI($embeds); - } - - /** - * @param string $uri - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool|string - */ - public function validate($uri, $config, $context) - { - if ($config->get('URI.Disable')) { - return false; - } - - $uri = $this->parseCDATA($uri); - - // parse the URI - $uri = $this->parser->parse($uri); - if ($uri === false) { - return false; - } - - // add embedded flag to context for validators - $context->register('EmbeddedURI', $this->embedsResource); - - $ok = false; - do { - - // generic validation - $result = $uri->validate($config, $context); - if (!$result) { - break; - } - - // chained filtering - $uri_def = $config->getDefinition('URI'); - $result = $uri_def->filter($uri, $config, $context); - if (!$result) { - break; - } - - // scheme-specific validation - $scheme_obj = $uri->getSchemeObj($config, $context); - if (!$scheme_obj) { - break; - } - if ($this->embedsResource && !$scheme_obj->browsable) { - break; - } - $result = $scheme_obj->validate($uri, $config, $context); - if (!$result) { - break; - } - - // Post chained filtering - $result = $uri_def->postFilter($uri, $config, $context); - if (!$result) { - break; - } - - // survived gauntlet - $ok = true; - - } while (false); - - $context->destroy('EmbeddedURI'); - if (!$ok) { - return false; - } - // back to string - return $uri->toString(); - } -} - -// vim: et sw=4 sts=4 +parser = new HTMLPurifier_URIParser(); + $this->embedsResource = (bool)$embeds_resource; + } + + /** + * @param string $string + * @return HTMLPurifier_AttrDef_URI + */ + public function make($string) + { + $embeds = ($string === 'embedded'); + return new HTMLPurifier_AttrDef_URI($embeds); + } + + /** + * @param string $uri + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($uri, $config, $context) + { + if ($config->get('URI.Disable')) { + return false; + } + + $uri = $this->parseCDATA($uri); + + // parse the URI + $uri = $this->parser->parse($uri); + if ($uri === false) { + return false; + } + + // add embedded flag to context for validators + $context->register('EmbeddedURI', $this->embedsResource); + + $ok = false; + do { + + // generic validation + $result = $uri->validate($config, $context); + if (!$result) { + break; + } + + // chained filtering + $uri_def = $config->getDefinition('URI'); + $result = $uri_def->filter($uri, $config, $context); + if (!$result) { + break; + } + + // scheme-specific validation + $scheme_obj = $uri->getSchemeObj($config, $context); + if (!$scheme_obj) { + break; + } + if ($this->embedsResource && !$scheme_obj->browsable) { + break; + } + $result = $scheme_obj->validate($uri, $config, $context); + if (!$result) { + break; + } + + // Post chained filtering + $result = $uri_def->postFilter($uri, $config, $context); + if (!$result) { + break; + } + + // survived gauntlet + $ok = true; + + } while (false); + + $context->destroy('EmbeddedURI'); + if (!$ok) { + return false; + } + // back to string + return $uri->toString(); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/URI/Email.php b/lib/purifier/HTMLPurifier/AttrDef/URI/Email.php index 846d388148..daf32b7643 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/URI/Email.php +++ b/lib/purifier/HTMLPurifier/AttrDef/URI/Email.php @@ -1,20 +1,20 @@ -" - // that needs more percent encoding to be done - if ($string == '') { - return false; - } - $string = trim($string); - $result = preg_match('/^[A-Z0-9._%-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i', $string); - return $result ? $string : false; - } -} - -// vim: et sw=4 sts=4 +" + // that needs more percent encoding to be done + if ($string == '') { + return false; + } + $string = trim($string); + $result = preg_match('/^[A-Z0-9._%-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i', $string); + return $result ? $string : false; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/URI/Host.php b/lib/purifier/HTMLPurifier/AttrDef/URI/Host.php index 487e3bf441..1beeaa5d22 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/URI/Host.php +++ b/lib/purifier/HTMLPurifier/AttrDef/URI/Host.php @@ -1,138 +1,142 @@ -ipv4 = new HTMLPurifier_AttrDef_URI_IPv4(); - $this->ipv6 = new HTMLPurifier_AttrDef_URI_IPv6(); - } - - /** - * @param string $string - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool|string - */ - public function validate($string, $config, $context) - { - $length = strlen($string); - // empty hostname is OK; it's usually semantically equivalent: - // the default host as defined by a URI scheme is used: - // - // If the URI scheme defines a default for host, then that - // default applies when the host subcomponent is undefined - // or when the registered name is empty (zero length). - if ($string === '') { - return ''; - } - if ($length > 1 && $string[0] === '[' && $string[$length - 1] === ']') { - //IPv6 - $ip = substr($string, 1, $length - 2); - $valid = $this->ipv6->validate($ip, $config, $context); - if ($valid === false) { - return false; - } - return '[' . $valid . ']'; - } - - // need to do checks on unusual encodings too - $ipv4 = $this->ipv4->validate($string, $config, $context); - if ($ipv4 !== false) { - return $ipv4; - } - - // A regular domain name. - - // This doesn't match I18N domain names, but we don't have proper IRI support, - // so force users to insert Punycode. - - // There is not a good sense in which underscores should be - // allowed, since it's technically not! (And if you go as - // far to allow everything as specified by the DNS spec... - // well, that's literally everything, modulo some space limits - // for the components and the overall name (which, by the way, - // we are NOT checking!). So we (arbitrarily) decide this: - // let's allow underscores wherever we would have allowed - // hyphens, if they are enabled. This is a pretty good match - // for browser behavior, for example, a large number of browsers - // cannot handle foo_.example.com, but foo_bar.example.com is - // fairly well supported. - $underscore = $config->get('Core.AllowHostnameUnderscore') ? '_' : ''; - - // Based off of RFC 1738, but amended so that - // as per RFC 3696, the top label need only not be all numeric. - // The productions describing this are: - $a = '[a-z]'; // alpha - $an = '[a-z0-9]'; // alphanum - $and = "[a-z0-9-$underscore]"; // alphanum | "-" - // domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum - $domainlabel = "$an(?:$and*$an)?"; - // AMENDED as per RFC 3696 - // toplabel = alphanum | alphanum *( alphanum | "-" ) alphanum - // side condition: not all numeric - $toplabel = "$an(?:$and*$an)?"; - // hostname = *( domainlabel "." ) toplabel [ "." ] - if (preg_match("/^(?:$domainlabel\.)*($toplabel)\.?$/i", $string, $matches)) { - if (!ctype_digit($matches[1])) { - return $string; - } - } - - // PHP 5.3 and later support this functionality natively - if (function_exists('idn_to_ascii')) { - $string = idn_to_ascii($string); - - // If we have Net_IDNA2 support, we can support IRIs by - // punycoding them. (This is the most portable thing to do, - // since otherwise we have to assume browsers support - } elseif ($config->get('Core.EnableIDNA')) { - $idna = new Net_IDNA2(array('encoding' => 'utf8', 'overlong' => false, 'strict' => true)); - // we need to encode each period separately - $parts = explode('.', $string); - try { - $new_parts = array(); - foreach ($parts as $part) { - $encodable = false; - for ($i = 0, $c = strlen($part); $i < $c; $i++) { - if (ord($part[$i]) > 0x7a) { - $encodable = true; - break; - } - } - if (!$encodable) { - $new_parts[] = $part; - } else { - $new_parts[] = $idna->encode($part); - } - } - $string = implode('.', $new_parts); - } catch (Exception $e) { - // XXX error reporting - } - } - // Try again - if (preg_match("/^($domainlabel\.)*$toplabel\.?$/i", $string)) { - return $string; - } - return false; - } -} - -// vim: et sw=4 sts=4 +ipv4 = new HTMLPurifier_AttrDef_URI_IPv4(); + $this->ipv6 = new HTMLPurifier_AttrDef_URI_IPv6(); + } + + /** + * @param string $string + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool|string + */ + public function validate($string, $config, $context) + { + $length = strlen($string); + // empty hostname is OK; it's usually semantically equivalent: + // the default host as defined by a URI scheme is used: + // + // If the URI scheme defines a default for host, then that + // default applies when the host subcomponent is undefined + // or when the registered name is empty (zero length). + if ($string === '') { + return ''; + } + if ($length > 1 && $string[0] === '[' && $string[$length - 1] === ']') { + //IPv6 + $ip = substr($string, 1, $length - 2); + $valid = $this->ipv6->validate($ip, $config, $context); + if ($valid === false) { + return false; + } + return '[' . $valid . ']'; + } + + // need to do checks on unusual encodings too + $ipv4 = $this->ipv4->validate($string, $config, $context); + if ($ipv4 !== false) { + return $ipv4; + } + + // A regular domain name. + + // This doesn't match I18N domain names, but we don't have proper IRI support, + // so force users to insert Punycode. + + // There is not a good sense in which underscores should be + // allowed, since it's technically not! (And if you go as + // far to allow everything as specified by the DNS spec... + // well, that's literally everything, modulo some space limits + // for the components and the overall name (which, by the way, + // we are NOT checking!). So we (arbitrarily) decide this: + // let's allow underscores wherever we would have allowed + // hyphens, if they are enabled. This is a pretty good match + // for browser behavior, for example, a large number of browsers + // cannot handle foo_.example.com, but foo_bar.example.com is + // fairly well supported. + $underscore = $config->get('Core.AllowHostnameUnderscore') ? '_' : ''; + + // Based off of RFC 1738, but amended so that + // as per RFC 3696, the top label need only not be all numeric. + // The productions describing this are: + $a = '[a-z]'; // alpha + $an = '[a-z0-9]'; // alphanum + $and = "[a-z0-9-$underscore]"; // alphanum | "-" + // domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum + $domainlabel = "$an(?:$and*$an)?"; + // AMENDED as per RFC 3696 + // toplabel = alphanum | alphanum *( alphanum | "-" ) alphanum + // side condition: not all numeric + $toplabel = "$an(?:$and*$an)?"; + // hostname = *( domainlabel "." ) toplabel [ "." ] + if (preg_match("/^(?:$domainlabel\.)*($toplabel)\.?$/i", $string, $matches)) { + if (!ctype_digit($matches[1])) { + return $string; + } + } + + // PHP 5.3 and later support this functionality natively + if (function_exists('idn_to_ascii')) { + if (defined('IDNA_NONTRANSITIONAL_TO_ASCII') && defined('INTL_IDNA_VARIANT_UTS46')) { + $string = idn_to_ascii($string, IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46); + } else { + $string = idn_to_ascii($string); + } + + // If we have Net_IDNA2 support, we can support IRIs by + // punycoding them. (This is the most portable thing to do, + // since otherwise we have to assume browsers support + } elseif ($config->get('Core.EnableIDNA')) { + $idna = new Net_IDNA2(array('encoding' => 'utf8', 'overlong' => false, 'strict' => true)); + // we need to encode each period separately + $parts = explode('.', $string); + try { + $new_parts = array(); + foreach ($parts as $part) { + $encodable = false; + for ($i = 0, $c = strlen($part); $i < $c; $i++) { + if (ord($part[$i]) > 0x7a) { + $encodable = true; + break; + } + } + if (!$encodable) { + $new_parts[] = $part; + } else { + $new_parts[] = $idna->encode($part); + } + } + $string = implode('.', $new_parts); + } catch (Exception $e) { + // XXX error reporting + } + } + // Try again + if (preg_match("/^($domainlabel\.)*$toplabel\.?$/i", $string)) { + return $string; + } + return false; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/URI/IPv4.php b/lib/purifier/HTMLPurifier/AttrDef/URI/IPv4.php index bbc8a77e32..30ac16c9e7 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/URI/IPv4.php +++ b/lib/purifier/HTMLPurifier/AttrDef/URI/IPv4.php @@ -1,45 +1,45 @@ -ip4) { - $this->_loadRegex(); - } - - if (preg_match('#^' . $this->ip4 . '$#s', $aIP)) { - return $aIP; - } - return false; - } - - /** - * Lazy load function to prevent regex from being stuffed in - * cache. - */ - protected function _loadRegex() - { - $oct = '(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])'; // 0-255 - $this->ip4 = "(?:{$oct}\\.{$oct}\\.{$oct}\\.{$oct})"; - } -} - -// vim: et sw=4 sts=4 +ip4) { + $this->_loadRegex(); + } + + if (preg_match('#^' . $this->ip4 . '$#s', $aIP)) { + return $aIP; + } + return false; + } + + /** + * Lazy load function to prevent regex from being stuffed in + * cache. + */ + protected function _loadRegex() + { + $oct = '(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])'; // 0-255 + $this->ip4 = "(?:{$oct}\\.{$oct}\\.{$oct}\\.{$oct})"; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrDef/URI/IPv6.php b/lib/purifier/HTMLPurifier/AttrDef/URI/IPv6.php index 67f148bd83..f243793eeb 100644 --- a/lib/purifier/HTMLPurifier/AttrDef/URI/IPv6.php +++ b/lib/purifier/HTMLPurifier/AttrDef/URI/IPv6.php @@ -1,89 +1,89 @@ -ip4) { - $this->_loadRegex(); - } - - $original = $aIP; - - $hex = '[0-9a-fA-F]'; - $blk = '(?:' . $hex . '{1,4})'; - $pre = '(?:/(?:12[0-8]|1[0-1][0-9]|[1-9][0-9]|[0-9]))'; // /0 - /128 - - // prefix check - if (strpos($aIP, '/') !== false) { - if (preg_match('#' . $pre . '$#s', $aIP, $find)) { - $aIP = substr($aIP, 0, 0 - strlen($find[0])); - unset($find); - } else { - return false; - } - } - - // IPv4-compatiblity check - if (preg_match('#(?<=:' . ')' . $this->ip4 . '$#s', $aIP, $find)) { - $aIP = substr($aIP, 0, 0 - strlen($find[0])); - $ip = explode('.', $find[0]); - $ip = array_map('dechex', $ip); - $aIP .= $ip[0] . $ip[1] . ':' . $ip[2] . $ip[3]; - unset($find, $ip); - } - - // compression check - $aIP = explode('::', $aIP); - $c = count($aIP); - if ($c > 2) { - return false; - } elseif ($c == 2) { - list($first, $second) = $aIP; - $first = explode(':', $first); - $second = explode(':', $second); - - if (count($first) + count($second) > 8) { - return false; - } - - while (count($first) < 8) { - array_push($first, '0'); - } - - array_splice($first, 8 - count($second), 8, $second); - $aIP = $first; - unset($first, $second); - } else { - $aIP = explode(':', $aIP[0]); - } - $c = count($aIP); - - if ($c != 8) { - return false; - } - - // All the pieces should be 16-bit hex strings. Are they? - foreach ($aIP as $piece) { - if (!preg_match('#^[0-9a-fA-F]{4}$#s', sprintf('%04s', $piece))) { - return false; - } - } - return $original; - } -} - -// vim: et sw=4 sts=4 +ip4) { + $this->_loadRegex(); + } + + $original = $aIP; + + $hex = '[0-9a-fA-F]'; + $blk = '(?:' . $hex . '{1,4})'; + $pre = '(?:/(?:12[0-8]|1[0-1][0-9]|[1-9][0-9]|[0-9]))'; // /0 - /128 + + // prefix check + if (strpos($aIP, '/') !== false) { + if (preg_match('#' . $pre . '$#s', $aIP, $find)) { + $aIP = substr($aIP, 0, 0 - strlen($find[0])); + unset($find); + } else { + return false; + } + } + + // IPv4-compatiblity check + if (preg_match('#(?<=:' . ')' . $this->ip4 . '$#s', $aIP, $find)) { + $aIP = substr($aIP, 0, 0 - strlen($find[0])); + $ip = explode('.', $find[0]); + $ip = array_map('dechex', $ip); + $aIP .= $ip[0] . $ip[1] . ':' . $ip[2] . $ip[3]; + unset($find, $ip); + } + + // compression check + $aIP = explode('::', $aIP); + $c = count($aIP); + if ($c > 2) { + return false; + } elseif ($c == 2) { + list($first, $second) = $aIP; + $first = explode(':', $first); + $second = explode(':', $second); + + if (count($first) + count($second) > 8) { + return false; + } + + while (count($first) < 8) { + array_push($first, '0'); + } + + array_splice($first, 8 - count($second), 8, $second); + $aIP = $first; + unset($first, $second); + } else { + $aIP = explode(':', $aIP[0]); + } + $c = count($aIP); + + if ($c != 8) { + return false; + } + + // All the pieces should be 16-bit hex strings. Are they? + foreach ($aIP as $piece) { + if (!preg_match('#^[0-9a-fA-F]{4}$#s', sprintf('%04s', $piece))) { + return false; + } + } + return $original; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrTransform.php b/lib/purifier/HTMLPurifier/AttrTransform.php index d9baaf394f..b428331f15 100644 --- a/lib/purifier/HTMLPurifier/AttrTransform.php +++ b/lib/purifier/HTMLPurifier/AttrTransform.php @@ -1,60 +1,60 @@ -confiscateAttr($attr, 'background'); - // some validation should happen here - - $this->prependCSS($attr, "background-image:url($background);"); - return $attr; - } -} - -// vim: et sw=4 sts=4 +confiscateAttr($attr, 'background'); + // some validation should happen here + + $this->prependCSS($attr, "background-image:url($background);"); + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrTransform/BdoDir.php b/lib/purifier/HTMLPurifier/AttrTransform/BdoDir.php index 86dcb17e4b..d66c04a5b8 100644 --- a/lib/purifier/HTMLPurifier/AttrTransform/BdoDir.php +++ b/lib/purifier/HTMLPurifier/AttrTransform/BdoDir.php @@ -1,27 +1,27 @@ -get('Attr.DefaultTextDir'); - return $attr; - } -} - -// vim: et sw=4 sts=4 +get('Attr.DefaultTextDir'); + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrTransform/BgColor.php b/lib/purifier/HTMLPurifier/AttrTransform/BgColor.php index e45e9ba376..0f51fd2cec 100644 --- a/lib/purifier/HTMLPurifier/AttrTransform/BgColor.php +++ b/lib/purifier/HTMLPurifier/AttrTransform/BgColor.php @@ -1,28 +1,28 @@ -confiscateAttr($attr, 'bgcolor'); - // some validation should happen here - - $this->prependCSS($attr, "background-color:$bgcolor;"); - return $attr; - } -} - -// vim: et sw=4 sts=4 +confiscateAttr($attr, 'bgcolor'); + // some validation should happen here + + $this->prependCSS($attr, "background-color:$bgcolor;"); + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrTransform/BoolToCSS.php b/lib/purifier/HTMLPurifier/AttrTransform/BoolToCSS.php index 29d7ff2698..f25cd01955 100644 --- a/lib/purifier/HTMLPurifier/AttrTransform/BoolToCSS.php +++ b/lib/purifier/HTMLPurifier/AttrTransform/BoolToCSS.php @@ -1,47 +1,47 @@ -attr = $attr; - $this->css = $css; - } - - /** - * @param array $attr - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return array - */ - public function transform($attr, $config, $context) - { - if (!isset($attr[$this->attr])) { - return $attr; - } - unset($attr[$this->attr]); - $this->prependCSS($attr, $this->css); - return $attr; - } -} - -// vim: et sw=4 sts=4 +attr = $attr; + $this->css = $css; + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (!isset($attr[$this->attr])) { + return $attr; + } + unset($attr[$this->attr]); + $this->prependCSS($attr, $this->css); + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrTransform/Border.php b/lib/purifier/HTMLPurifier/AttrTransform/Border.php index 90a8dea877..057dc017fa 100644 --- a/lib/purifier/HTMLPurifier/AttrTransform/Border.php +++ b/lib/purifier/HTMLPurifier/AttrTransform/Border.php @@ -1,26 +1,26 @@ -confiscateAttr($attr, 'border'); - // some validation should happen here - $this->prependCSS($attr, "border:{$border_width}px solid;"); - return $attr; - } -} - -// vim: et sw=4 sts=4 +confiscateAttr($attr, 'border'); + // some validation should happen here + $this->prependCSS($attr, "border:{$border_width}px solid;"); + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrTransform/EnumToCSS.php b/lib/purifier/HTMLPurifier/AttrTransform/EnumToCSS.php index e2bfbf0072..7ccd0e3fb7 100644 --- a/lib/purifier/HTMLPurifier/AttrTransform/EnumToCSS.php +++ b/lib/purifier/HTMLPurifier/AttrTransform/EnumToCSS.php @@ -1,68 +1,68 @@ -attr = $attr; - $this->enumToCSS = $enum_to_css; - $this->caseSensitive = (bool)$case_sensitive; - } - - /** - * @param array $attr - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return array - */ - public function transform($attr, $config, $context) - { - if (!isset($attr[$this->attr])) { - return $attr; - } - - $value = trim($attr[$this->attr]); - unset($attr[$this->attr]); - - if (!$this->caseSensitive) { - $value = strtolower($value); - } - - if (!isset($this->enumToCSS[$value])) { - return $attr; - } - $this->prependCSS($attr, $this->enumToCSS[$value]); - return $attr; - } -} - -// vim: et sw=4 sts=4 +attr = $attr; + $this->enumToCSS = $enum_to_css; + $this->caseSensitive = (bool)$case_sensitive; + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (!isset($attr[$this->attr])) { + return $attr; + } + + $value = trim($attr[$this->attr]); + unset($attr[$this->attr]); + + if (!$this->caseSensitive) { + $value = strtolower($value); + } + + if (!isset($this->enumToCSS[$value])) { + return $attr; + } + $this->prependCSS($attr, $this->enumToCSS[$value]); + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrTransform/ImgRequired.php b/lib/purifier/HTMLPurifier/AttrTransform/ImgRequired.php index 335f0033e1..235ebb34b6 100644 --- a/lib/purifier/HTMLPurifier/AttrTransform/ImgRequired.php +++ b/lib/purifier/HTMLPurifier/AttrTransform/ImgRequired.php @@ -1,47 +1,47 @@ -get('Core.RemoveInvalidImg')) { - return $attr; - } - $attr['src'] = $config->get('Attr.DefaultInvalidImage'); - $src = false; - } - - if (!isset($attr['alt'])) { - if ($src) { - $alt = $config->get('Attr.DefaultImageAlt'); - if ($alt === null) { - $attr['alt'] = basename($attr['src']); - } else { - $attr['alt'] = $alt; - } - } else { - $attr['alt'] = $config->get('Attr.DefaultInvalidImageAlt'); - } - } - return $attr; - } -} - -// vim: et sw=4 sts=4 +get('Core.RemoveInvalidImg')) { + return $attr; + } + $attr['src'] = $config->get('Attr.DefaultInvalidImage'); + $src = false; + } + + if (!isset($attr['alt'])) { + if ($src) { + $alt = $config->get('Attr.DefaultImageAlt'); + if ($alt === null) { + $attr['alt'] = basename($attr['src']); + } else { + $attr['alt'] = $alt; + } + } else { + $attr['alt'] = $config->get('Attr.DefaultInvalidImageAlt'); + } + } + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrTransform/ImgSpace.php b/lib/purifier/HTMLPurifier/AttrTransform/ImgSpace.php index aec42aea8c..350b3358fc 100644 --- a/lib/purifier/HTMLPurifier/AttrTransform/ImgSpace.php +++ b/lib/purifier/HTMLPurifier/AttrTransform/ImgSpace.php @@ -1,61 +1,61 @@ - array('left', 'right'), - 'vspace' => array('top', 'bottom') - ); - - /** - * @param string $attr - */ - public function __construct($attr) - { - $this->attr = $attr; - if (!isset($this->css[$attr])) { - trigger_error(htmlspecialchars($attr) . ' is not valid space attribute'); - } - } - - /** - * @param array $attr - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return array - */ - public function transform($attr, $config, $context) - { - if (!isset($attr[$this->attr])) { - return $attr; - } - - $width = $this->confiscateAttr($attr, $this->attr); - // some validation could happen here - - if (!isset($this->css[$this->attr])) { - return $attr; - } - - $style = ''; - foreach ($this->css[$this->attr] as $suffix) { - $property = "margin-$suffix"; - $style .= "$property:{$width}px;"; - } - $this->prependCSS($attr, $style); - return $attr; - } -} - -// vim: et sw=4 sts=4 + array('left', 'right'), + 'vspace' => array('top', 'bottom') + ); + + /** + * @param string $attr + */ + public function __construct($attr) + { + $this->attr = $attr; + if (!isset($this->css[$attr])) { + trigger_error(htmlspecialchars($attr) . ' is not valid space attribute'); + } + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (!isset($attr[$this->attr])) { + return $attr; + } + + $width = $this->confiscateAttr($attr, $this->attr); + // some validation could happen here + + if (!isset($this->css[$this->attr])) { + return $attr; + } + + $style = ''; + foreach ($this->css[$this->attr] as $suffix) { + $property = "margin-$suffix"; + $style .= "$property:{$width}px;"; + } + $this->prependCSS($attr, $style); + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrTransform/Input.php b/lib/purifier/HTMLPurifier/AttrTransform/Input.php index 17a2ce4cec..3ab47ed8c9 100644 --- a/lib/purifier/HTMLPurifier/AttrTransform/Input.php +++ b/lib/purifier/HTMLPurifier/AttrTransform/Input.php @@ -1,56 +1,56 @@ -pixels = new HTMLPurifier_AttrDef_HTML_Pixels(); - } - - /** - * @param array $attr - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return array - */ - public function transform($attr, $config, $context) - { - if (!isset($attr['type'])) { - $t = 'text'; - } else { - $t = strtolower($attr['type']); - } - if (isset($attr['checked']) && $t !== 'radio' && $t !== 'checkbox') { - unset($attr['checked']); - } - if (isset($attr['maxlength']) && $t !== 'text' && $t !== 'password') { - unset($attr['maxlength']); - } - if (isset($attr['size']) && $t !== 'text' && $t !== 'password') { - $result = $this->pixels->validate($attr['size'], $config, $context); - if ($result === false) { - unset($attr['size']); - } else { - $attr['size'] = $result; - } - } - if (isset($attr['src']) && $t !== 'image') { - unset($attr['src']); - } - if (!isset($attr['value']) && ($t === 'radio' || $t === 'checkbox')) { - $attr['value'] = ''; - } - return $attr; - } -} - -// vim: et sw=4 sts=4 +pixels = new HTMLPurifier_AttrDef_HTML_Pixels(); + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (!isset($attr['type'])) { + $t = 'text'; + } else { + $t = strtolower($attr['type']); + } + if (isset($attr['checked']) && $t !== 'radio' && $t !== 'checkbox') { + unset($attr['checked']); + } + if (isset($attr['maxlength']) && $t !== 'text' && $t !== 'password') { + unset($attr['maxlength']); + } + if (isset($attr['size']) && $t !== 'text' && $t !== 'password') { + $result = $this->pixels->validate($attr['size'], $config, $context); + if ($result === false) { + unset($attr['size']); + } else { + $attr['size'] = $result; + } + } + if (isset($attr['src']) && $t !== 'image') { + unset($attr['src']); + } + if (!isset($attr['value']) && ($t === 'radio' || $t === 'checkbox')) { + $attr['value'] = ''; + } + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrTransform/Lang.php b/lib/purifier/HTMLPurifier/AttrTransform/Lang.php index 591b8ca738..5b0aff0e40 100644 --- a/lib/purifier/HTMLPurifier/AttrTransform/Lang.php +++ b/lib/purifier/HTMLPurifier/AttrTransform/Lang.php @@ -1,31 +1,31 @@ -name = $name; - $this->cssName = $css_name ? $css_name : $name; - } - - /** - * @param array $attr - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return array - */ - public function transform($attr, $config, $context) - { - if (!isset($attr[$this->name])) { - return $attr; - } - $length = $this->confiscateAttr($attr, $this->name); - if (ctype_digit($length)) { - $length .= 'px'; - } - $this->prependCSS($attr, $this->cssName . ":$length;"); - return $attr; - } -} - -// vim: et sw=4 sts=4 +name = $name; + $this->cssName = $css_name ? $css_name : $name; + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (!isset($attr[$this->name])) { + return $attr; + } + $length = $this->confiscateAttr($attr, $this->name); + if (ctype_digit($length)) { + $length .= 'px'; + } + $this->prependCSS($attr, $this->cssName . ":$length;"); + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrTransform/Name.php b/lib/purifier/HTMLPurifier/AttrTransform/Name.php index a874d0f7ae..63cce6837a 100644 --- a/lib/purifier/HTMLPurifier/AttrTransform/Name.php +++ b/lib/purifier/HTMLPurifier/AttrTransform/Name.php @@ -1,33 +1,33 @@ -get('HTML.Attr.Name.UseCDATA')) { - return $attr; - } - if (!isset($attr['name'])) { - return $attr; - } - $id = $this->confiscateAttr($attr, 'name'); - if (isset($attr['id'])) { - return $attr; - } - $attr['id'] = $id; - return $attr; - } -} - -// vim: et sw=4 sts=4 +get('HTML.Attr.Name.UseCDATA')) { + return $attr; + } + if (!isset($attr['name'])) { + return $attr; + } + $id = $this->confiscateAttr($attr, 'name'); + if (isset($attr['id'])) { + return $attr; + } + $attr['id'] = $id; + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrTransform/NameSync.php b/lib/purifier/HTMLPurifier/AttrTransform/NameSync.php index 457f8110f0..36079b786f 100644 --- a/lib/purifier/HTMLPurifier/AttrTransform/NameSync.php +++ b/lib/purifier/HTMLPurifier/AttrTransform/NameSync.php @@ -1,41 +1,41 @@ -idDef = new HTMLPurifier_AttrDef_HTML_ID(); - } - - /** - * @param array $attr - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return array - */ - public function transform($attr, $config, $context) - { - if (!isset($attr['name'])) { - return $attr; - } - $name = $attr['name']; - if (isset($attr['id']) && $attr['id'] === $name) { - return $attr; - } - $result = $this->idDef->validate($name, $config, $context); - if ($result === false) { - unset($attr['name']); - } else { - $attr['name'] = $result; - } - return $attr; - } -} - -// vim: et sw=4 sts=4 +idDef = new HTMLPurifier_AttrDef_HTML_ID(); + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (!isset($attr['name'])) { + return $attr; + } + $name = $attr['name']; + if (isset($attr['id']) && $attr['id'] === $name) { + return $attr; + } + $result = $this->idDef->validate($name, $config, $context); + if ($result === false) { + unset($attr['name']); + } else { + $attr['name'] = $result; + } + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrTransform/Nofollow.php b/lib/purifier/HTMLPurifier/AttrTransform/Nofollow.php index 25173c219f..1057ebee1b 100644 --- a/lib/purifier/HTMLPurifier/AttrTransform/Nofollow.php +++ b/lib/purifier/HTMLPurifier/AttrTransform/Nofollow.php @@ -1,52 +1,52 @@ -parser = new HTMLPurifier_URIParser(); - } - - /** - * @param array $attr - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return array - */ - public function transform($attr, $config, $context) - { - if (!isset($attr['href'])) { - return $attr; - } - - // XXX Kind of inefficient - $url = $this->parser->parse($attr['href']); - $scheme = $url->getSchemeObj($config, $context); - - if ($scheme->browsable && !$url->isLocal($config, $context)) { - if (isset($attr['rel'])) { - $rels = explode(' ', $attr['rel']); - if (!in_array('nofollow', $rels)) { - $rels[] = 'nofollow'; - } - $attr['rel'] = implode(' ', $rels); - } else { - $attr['rel'] = 'nofollow'; - } - } - return $attr; - } -} - -// vim: et sw=4 sts=4 +parser = new HTMLPurifier_URIParser(); + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (!isset($attr['href'])) { + return $attr; + } + + // XXX Kind of inefficient + $url = $this->parser->parse($attr['href']); + $scheme = $url->getSchemeObj($config, $context); + + if ($scheme->browsable && !$url->isLocal($config, $context)) { + if (isset($attr['rel'])) { + $rels = explode(' ', $attr['rel']); + if (!in_array('nofollow', $rels)) { + $rels[] = 'nofollow'; + } + $attr['rel'] = implode(' ', $rels); + } else { + $attr['rel'] = 'nofollow'; + } + } + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrTransform/SafeEmbed.php b/lib/purifier/HTMLPurifier/AttrTransform/SafeEmbed.php index 98ebf49bfb..231c81a3f0 100644 --- a/lib/purifier/HTMLPurifier/AttrTransform/SafeEmbed.php +++ b/lib/purifier/HTMLPurifier/AttrTransform/SafeEmbed.php @@ -1,25 +1,25 @@ -uri = new HTMLPurifier_AttrDef_URI(true); // embedded - $this->wmode = new HTMLPurifier_AttrDef_Enum(array('window', 'opaque', 'transparent')); - } - - /** - * @param array $attr - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return array - */ - public function transform($attr, $config, $context) - { - // If we add support for other objects, we'll need to alter the - // transforms. - switch ($attr['name']) { - // application/x-shockwave-flash - // Keep this synchronized with Injector/SafeObject.php - case 'allowScriptAccess': - $attr['value'] = 'never'; - break; - case 'allowNetworking': - $attr['value'] = 'internal'; - break; - case 'allowFullScreen': - if ($config->get('HTML.FlashAllowFullScreen')) { - $attr['value'] = ($attr['value'] == 'true') ? 'true' : 'false'; - } else { - $attr['value'] = 'false'; - } - break; - case 'wmode': - $attr['value'] = $this->wmode->validate($attr['value'], $config, $context); - break; - case 'movie': - case 'src': - $attr['name'] = "movie"; - $attr['value'] = $this->uri->validate($attr['value'], $config, $context); - break; - case 'flashvars': - // we're going to allow arbitrary inputs to the SWF, on - // the reasoning that it could only hack the SWF, not us. - break; - // add other cases to support other param name/value pairs - default: - $attr['name'] = $attr['value'] = null; - } - return $attr; - } -} - -// vim: et sw=4 sts=4 +uri = new HTMLPurifier_AttrDef_URI(true); // embedded + $this->wmode = new HTMLPurifier_AttrDef_Enum(array('window', 'opaque', 'transparent')); + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + // If we add support for other objects, we'll need to alter the + // transforms. + switch ($attr['name']) { + // application/x-shockwave-flash + // Keep this synchronized with Injector/SafeObject.php + case 'allowScriptAccess': + $attr['value'] = 'never'; + break; + case 'allowNetworking': + $attr['value'] = 'internal'; + break; + case 'allowFullScreen': + if ($config->get('HTML.FlashAllowFullScreen')) { + $attr['value'] = ($attr['value'] == 'true') ? 'true' : 'false'; + } else { + $attr['value'] = 'false'; + } + break; + case 'wmode': + $attr['value'] = $this->wmode->validate($attr['value'], $config, $context); + break; + case 'movie': + case 'src': + $attr['name'] = "movie"; + $attr['value'] = $this->uri->validate($attr['value'], $config, $context); + break; + case 'flashvars': + // we're going to allow arbitrary inputs to the SWF, on + // the reasoning that it could only hack the SWF, not us. + break; + // add other cases to support other param name/value pairs + default: + $attr['name'] = $attr['value'] = null; + } + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrTransform/ScriptRequired.php b/lib/purifier/HTMLPurifier/AttrTransform/ScriptRequired.php index 49445b43f1..b7057bbf8e 100644 --- a/lib/purifier/HTMLPurifier/AttrTransform/ScriptRequired.php +++ b/lib/purifier/HTMLPurifier/AttrTransform/ScriptRequired.php @@ -1,23 +1,23 @@ - - */ -class HTMLPurifier_AttrTransform_ScriptRequired extends HTMLPurifier_AttrTransform -{ - /** - * @param array $attr - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return array - */ - public function transform($attr, $config, $context) - { - if (!isset($attr['type'])) { - $attr['type'] = 'text/javascript'; - } - return $attr; - } -} - -// vim: et sw=4 sts=4 + + */ +class HTMLPurifier_AttrTransform_ScriptRequired extends HTMLPurifier_AttrTransform +{ + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (!isset($attr['type'])) { + $attr['type'] = 'text/javascript'; + } + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrTransform/TargetBlank.php b/lib/purifier/HTMLPurifier/AttrTransform/TargetBlank.php index f66dcf8c4e..dd63ea89cb 100644 --- a/lib/purifier/HTMLPurifier/AttrTransform/TargetBlank.php +++ b/lib/purifier/HTMLPurifier/AttrTransform/TargetBlank.php @@ -1,45 +1,45 @@ -parser = new HTMLPurifier_URIParser(); - } - - /** - * @param array $attr - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return array - */ - public function transform($attr, $config, $context) - { - if (!isset($attr['href'])) { - return $attr; - } - - // XXX Kind of inefficient - $url = $this->parser->parse($attr['href']); - $scheme = $url->getSchemeObj($config, $context); - - if ($scheme->browsable && !$url->isBenign($config, $context)) { - $attr['target'] = '_blank'; - } - return $attr; - } -} - -// vim: et sw=4 sts=4 +parser = new HTMLPurifier_URIParser(); + } + + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + if (!isset($attr['href'])) { + return $attr; + } + + // XXX Kind of inefficient + $url = $this->parser->parse($attr['href']); + $scheme = $url->getSchemeObj($config, $context); + + if ($scheme->browsable && !$url->isBenign($config, $context)) { + $attr['target'] = '_blank'; + } + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrTransform/TargetNoopener.php b/lib/purifier/HTMLPurifier/AttrTransform/TargetNoopener.php index ab4c09721c..1db3c6c09e 100644 --- a/lib/purifier/HTMLPurifier/AttrTransform/TargetNoopener.php +++ b/lib/purifier/HTMLPurifier/AttrTransform/TargetNoopener.php @@ -1,37 +1,37 @@ - - */ -class HTMLPurifier_AttrTransform_Textarea extends HTMLPurifier_AttrTransform -{ - /** - * @param array $attr - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return array - */ - public function transform($attr, $config, $context) - { - // Calculated from Firefox - if (!isset($attr['cols'])) { - $attr['cols'] = '22'; - } - if (!isset($attr['rows'])) { - $attr['rows'] = '3'; - } - return $attr; - } -} - -// vim: et sw=4 sts=4 + + */ +class HTMLPurifier_AttrTransform_Textarea extends HTMLPurifier_AttrTransform +{ + /** + * @param array $attr + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function transform($attr, $config, $context) + { + // Calculated from Firefox + if (!isset($attr['cols'])) { + $attr['cols'] = '22'; + } + if (!isset($attr['rows'])) { + $attr['rows'] = '3'; + } + return $attr; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrTypes.php b/lib/purifier/HTMLPurifier/AttrTypes.php index 7e8019bd0b..3b70520b6a 100644 --- a/lib/purifier/HTMLPurifier/AttrTypes.php +++ b/lib/purifier/HTMLPurifier/AttrTypes.php @@ -1,96 +1,96 @@ -info['Enum'] = new HTMLPurifier_AttrDef_Enum(); - $this->info['Bool'] = new HTMLPurifier_AttrDef_HTML_Bool(); - - $this->info['CDATA'] = new HTMLPurifier_AttrDef_Text(); - $this->info['ID'] = new HTMLPurifier_AttrDef_HTML_ID(); - $this->info['Length'] = new HTMLPurifier_AttrDef_HTML_Length(); - $this->info['MultiLength'] = new HTMLPurifier_AttrDef_HTML_MultiLength(); - $this->info['NMTOKENS'] = new HTMLPurifier_AttrDef_HTML_Nmtokens(); - $this->info['Pixels'] = new HTMLPurifier_AttrDef_HTML_Pixels(); - $this->info['Text'] = new HTMLPurifier_AttrDef_Text(); - $this->info['URI'] = new HTMLPurifier_AttrDef_URI(); - $this->info['LanguageCode'] = new HTMLPurifier_AttrDef_Lang(); - $this->info['Color'] = new HTMLPurifier_AttrDef_HTML_Color(); - $this->info['IAlign'] = self::makeEnum('top,middle,bottom,left,right'); - $this->info['LAlign'] = self::makeEnum('top,bottom,left,right'); - $this->info['FrameTarget'] = new HTMLPurifier_AttrDef_HTML_FrameTarget(); - - // unimplemented aliases - $this->info['ContentType'] = new HTMLPurifier_AttrDef_Text(); - $this->info['ContentTypes'] = new HTMLPurifier_AttrDef_Text(); - $this->info['Charsets'] = new HTMLPurifier_AttrDef_Text(); - $this->info['Character'] = new HTMLPurifier_AttrDef_Text(); - - // "proprietary" types - $this->info['Class'] = new HTMLPurifier_AttrDef_HTML_Class(); - - // number is really a positive integer (one or more digits) - // FIXME: ^^ not always, see start and value of list items - $this->info['Number'] = new HTMLPurifier_AttrDef_Integer(false, false, true); - } - - private static function makeEnum($in) - { - return new HTMLPurifier_AttrDef_Clone(new HTMLPurifier_AttrDef_Enum(explode(',', $in))); - } - - /** - * Retrieves a type - * @param string $type String type name - * @return HTMLPurifier_AttrDef Object AttrDef for type - */ - public function get($type) - { - // determine if there is any extra info tacked on - if (strpos($type, '#') !== false) { - list($type, $string) = explode('#', $type, 2); - } else { - $string = ''; - } - - if (!isset($this->info[$type])) { - trigger_error('Cannot retrieve undefined attribute type ' . $type, E_USER_ERROR); - return; - } - return $this->info[$type]->make($string); - } - - /** - * Sets a new implementation for a type - * @param string $type String type name - * @param HTMLPurifier_AttrDef $impl Object AttrDef for type - */ - public function set($type, $impl) - { - $this->info[$type] = $impl; - } -} - -// vim: et sw=4 sts=4 +info['Enum'] = new HTMLPurifier_AttrDef_Enum(); + $this->info['Bool'] = new HTMLPurifier_AttrDef_HTML_Bool(); + + $this->info['CDATA'] = new HTMLPurifier_AttrDef_Text(); + $this->info['ID'] = new HTMLPurifier_AttrDef_HTML_ID(); + $this->info['Length'] = new HTMLPurifier_AttrDef_HTML_Length(); + $this->info['MultiLength'] = new HTMLPurifier_AttrDef_HTML_MultiLength(); + $this->info['NMTOKENS'] = new HTMLPurifier_AttrDef_HTML_Nmtokens(); + $this->info['Pixels'] = new HTMLPurifier_AttrDef_HTML_Pixels(); + $this->info['Text'] = new HTMLPurifier_AttrDef_Text(); + $this->info['URI'] = new HTMLPurifier_AttrDef_URI(); + $this->info['LanguageCode'] = new HTMLPurifier_AttrDef_Lang(); + $this->info['Color'] = new HTMLPurifier_AttrDef_HTML_Color(); + $this->info['IAlign'] = self::makeEnum('top,middle,bottom,left,right'); + $this->info['LAlign'] = self::makeEnum('top,bottom,left,right'); + $this->info['FrameTarget'] = new HTMLPurifier_AttrDef_HTML_FrameTarget(); + + // unimplemented aliases + $this->info['ContentType'] = new HTMLPurifier_AttrDef_Text(); + $this->info['ContentTypes'] = new HTMLPurifier_AttrDef_Text(); + $this->info['Charsets'] = new HTMLPurifier_AttrDef_Text(); + $this->info['Character'] = new HTMLPurifier_AttrDef_Text(); + + // "proprietary" types + $this->info['Class'] = new HTMLPurifier_AttrDef_HTML_Class(); + + // number is really a positive integer (one or more digits) + // FIXME: ^^ not always, see start and value of list items + $this->info['Number'] = new HTMLPurifier_AttrDef_Integer(false, false, true); + } + + private static function makeEnum($in) + { + return new HTMLPurifier_AttrDef_Clone(new HTMLPurifier_AttrDef_Enum(explode(',', $in))); + } + + /** + * Retrieves a type + * @param string $type String type name + * @return HTMLPurifier_AttrDef Object AttrDef for type + */ + public function get($type) + { + // determine if there is any extra info tacked on + if (strpos($type, '#') !== false) { + list($type, $string) = explode('#', $type, 2); + } else { + $string = ''; + } + + if (!isset($this->info[$type])) { + trigger_error('Cannot retrieve undefined attribute type ' . $type, E_USER_ERROR); + return; + } + return $this->info[$type]->make($string); + } + + /** + * Sets a new implementation for a type + * @param string $type String type name + * @param HTMLPurifier_AttrDef $impl Object AttrDef for type + */ + public function set($type, $impl) + { + $this->info[$type] = $impl; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/AttrValidator.php b/lib/purifier/HTMLPurifier/AttrValidator.php index 1a2b0b673e..f97dc93edd 100644 --- a/lib/purifier/HTMLPurifier/AttrValidator.php +++ b/lib/purifier/HTMLPurifier/AttrValidator.php @@ -1,178 +1,178 @@ -getHTMLDefinition(); - $e =& $context->get('ErrorCollector', true); - - // initialize IDAccumulator if necessary - $ok =& $context->get('IDAccumulator', true); - if (!$ok) { - $id_accumulator = HTMLPurifier_IDAccumulator::build($config, $context); - $context->register('IDAccumulator', $id_accumulator); - } - - // initialize CurrentToken if necessary - $current_token =& $context->get('CurrentToken', true); - if (!$current_token) { - $context->register('CurrentToken', $token); - } - - if (!$token instanceof HTMLPurifier_Token_Start && - !$token instanceof HTMLPurifier_Token_Empty - ) { - return; - } - - // create alias to global definition array, see also $defs - // DEFINITION CALL - $d_defs = $definition->info_global_attr; - - // don't update token until the very end, to ensure an atomic update - $attr = $token->attr; - - // do global transformations (pre) - // nothing currently utilizes this - foreach ($definition->info_attr_transform_pre as $transform) { - $attr = $transform->transform($o = $attr, $config, $context); - if ($e) { - if ($attr != $o) { - $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); - } - } - } - - // do local transformations only applicable to this element (pre) - // ex.

to

- foreach ($definition->info[$token->name]->attr_transform_pre as $transform) { - $attr = $transform->transform($o = $attr, $config, $context); - if ($e) { - if ($attr != $o) { - $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); - } - } - } - - // create alias to this element's attribute definition array, see - // also $d_defs (global attribute definition array) - // DEFINITION CALL - $defs = $definition->info[$token->name]->attr; - - $attr_key = false; - $context->register('CurrentAttr', $attr_key); - - // iterate through all the attribute keypairs - // Watch out for name collisions: $key has previously been used - foreach ($attr as $attr_key => $value) { - - // call the definition - if (isset($defs[$attr_key])) { - // there is a local definition defined - if ($defs[$attr_key] === false) { - // We've explicitly been told not to allow this element. - // This is usually when there's a global definition - // that must be overridden. - // Theoretically speaking, we could have a - // AttrDef_DenyAll, but this is faster! - $result = false; - } else { - // validate according to the element's definition - $result = $defs[$attr_key]->validate( - $value, - $config, - $context - ); - } - } elseif (isset($d_defs[$attr_key])) { - // there is a global definition defined, validate according - // to the global definition - $result = $d_defs[$attr_key]->validate( - $value, - $config, - $context - ); - } else { - // system never heard of the attribute? DELETE! - $result = false; - } - - // put the results into effect - if ($result === false || $result === null) { - // this is a generic error message that should replaced - // with more specific ones when possible - if ($e) { - $e->send(E_ERROR, 'AttrValidator: Attribute removed'); - } - - // remove the attribute - unset($attr[$attr_key]); - } elseif (is_string($result)) { - // generally, if a substitution is happening, there - // was some sort of implicit correction going on. We'll - // delegate it to the attribute classes to say exactly what. - - // simple substitution - $attr[$attr_key] = $result; - } else { - // nothing happens - } - - // we'd also want slightly more complicated substitution - // involving an array as the return value, - // although we're not sure how colliding attributes would - // resolve (certain ones would be completely overriden, - // others would prepend themselves). - } - - $context->destroy('CurrentAttr'); - - // post transforms - - // global (error reporting untested) - foreach ($definition->info_attr_transform_post as $transform) { - $attr = $transform->transform($o = $attr, $config, $context); - if ($e) { - if ($attr != $o) { - $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); - } - } - } - - // local (error reporting untested) - foreach ($definition->info[$token->name]->attr_transform_post as $transform) { - $attr = $transform->transform($o = $attr, $config, $context); - if ($e) { - if ($attr != $o) { - $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); - } - } - } - - $token->attr = $attr; - - // destroy CurrentToken if we made it ourselves - if (!$current_token) { - $context->destroy('CurrentToken'); - } - - } - - -} - -// vim: et sw=4 sts=4 +getHTMLDefinition(); + $e =& $context->get('ErrorCollector', true); + + // initialize IDAccumulator if necessary + $ok =& $context->get('IDAccumulator', true); + if (!$ok) { + $id_accumulator = HTMLPurifier_IDAccumulator::build($config, $context); + $context->register('IDAccumulator', $id_accumulator); + } + + // initialize CurrentToken if necessary + $current_token =& $context->get('CurrentToken', true); + if (!$current_token) { + $context->register('CurrentToken', $token); + } + + if (!$token instanceof HTMLPurifier_Token_Start && + !$token instanceof HTMLPurifier_Token_Empty + ) { + return; + } + + // create alias to global definition array, see also $defs + // DEFINITION CALL + $d_defs = $definition->info_global_attr; + + // don't update token until the very end, to ensure an atomic update + $attr = $token->attr; + + // do global transformations (pre) + // nothing currently utilizes this + foreach ($definition->info_attr_transform_pre as $transform) { + $attr = $transform->transform($o = $attr, $config, $context); + if ($e) { + if ($attr != $o) { + $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); + } + } + } + + // do local transformations only applicable to this element (pre) + // ex.

to

+ foreach ($definition->info[$token->name]->attr_transform_pre as $transform) { + $attr = $transform->transform($o = $attr, $config, $context); + if ($e) { + if ($attr != $o) { + $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); + } + } + } + + // create alias to this element's attribute definition array, see + // also $d_defs (global attribute definition array) + // DEFINITION CALL + $defs = $definition->info[$token->name]->attr; + + $attr_key = false; + $context->register('CurrentAttr', $attr_key); + + // iterate through all the attribute keypairs + // Watch out for name collisions: $key has previously been used + foreach ($attr as $attr_key => $value) { + + // call the definition + if (isset($defs[$attr_key])) { + // there is a local definition defined + if ($defs[$attr_key] === false) { + // We've explicitly been told not to allow this element. + // This is usually when there's a global definition + // that must be overridden. + // Theoretically speaking, we could have a + // AttrDef_DenyAll, but this is faster! + $result = false; + } else { + // validate according to the element's definition + $result = $defs[$attr_key]->validate( + $value, + $config, + $context + ); + } + } elseif (isset($d_defs[$attr_key])) { + // there is a global definition defined, validate according + // to the global definition + $result = $d_defs[$attr_key]->validate( + $value, + $config, + $context + ); + } else { + // system never heard of the attribute? DELETE! + $result = false; + } + + // put the results into effect + if ($result === false || $result === null) { + // this is a generic error message that should replaced + // with more specific ones when possible + if ($e) { + $e->send(E_ERROR, 'AttrValidator: Attribute removed'); + } + + // remove the attribute + unset($attr[$attr_key]); + } elseif (is_string($result)) { + // generally, if a substitution is happening, there + // was some sort of implicit correction going on. We'll + // delegate it to the attribute classes to say exactly what. + + // simple substitution + $attr[$attr_key] = $result; + } else { + // nothing happens + } + + // we'd also want slightly more complicated substitution + // involving an array as the return value, + // although we're not sure how colliding attributes would + // resolve (certain ones would be completely overriden, + // others would prepend themselves). + } + + $context->destroy('CurrentAttr'); + + // post transforms + + // global (error reporting untested) + foreach ($definition->info_attr_transform_post as $transform) { + $attr = $transform->transform($o = $attr, $config, $context); + if ($e) { + if ($attr != $o) { + $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); + } + } + } + + // local (error reporting untested) + foreach ($definition->info[$token->name]->attr_transform_post as $transform) { + $attr = $transform->transform($o = $attr, $config, $context); + if ($e) { + if ($attr != $o) { + $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); + } + } + } + + $token->attr = $attr; + + // destroy CurrentToken if we made it ourselves + if (!$current_token) { + $context->destroy('CurrentToken'); + } + + } + + +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/Bootstrap.php b/lib/purifier/HTMLPurifier/Bootstrap.php index 0ab0e341e7..707122bb29 100644 --- a/lib/purifier/HTMLPurifier/Bootstrap.php +++ b/lib/purifier/HTMLPurifier/Bootstrap.php @@ -1,124 +1,124 @@ - -if (!defined('PHP_EOL')) { - switch (strtoupper(substr(PHP_OS, 0, 3))) { - case 'WIN': - define('PHP_EOL', "\r\n"); - break; - case 'DAR': - define('PHP_EOL', "\r"); - break; - default: - define('PHP_EOL', "\n"); - } -} - -/** - * Bootstrap class that contains meta-functionality for HTML Purifier such as - * the autoload function. - * - * @note - * This class may be used without any other files from HTML Purifier. - */ -class HTMLPurifier_Bootstrap -{ - - /** - * Autoload function for HTML Purifier - * @param string $class Class to load - * @return bool - */ - public static function autoload($class) - { - $file = HTMLPurifier_Bootstrap::getPath($class); - if (!$file) { - return false; - } - // Technically speaking, it should be ok and more efficient to - // just do 'require', but Antonio Parraga reports that with - // Zend extensions such as Zend debugger and APC, this invariant - // may be broken. Since we have efficient alternatives, pay - // the cost here and avoid the bug. - require_once HTMLPURIFIER_PREFIX . '/' . $file; - return true; - } - - /** - * Returns the path for a specific class. - * @param string $class Class path to get - * @return string - */ - public static function getPath($class) - { - if (strncmp('HTMLPurifier', $class, 12) !== 0) { - return false; - } - // Custom implementations - if (strncmp('HTMLPurifier_Language_', $class, 22) === 0) { - $code = str_replace('_', '-', substr($class, 22)); - $file = 'HTMLPurifier/Language/classes/' . $code . '.php'; - } else { - $file = str_replace('_', '/', $class) . '.php'; - } - if (!file_exists(HTMLPURIFIER_PREFIX . '/' . $file)) { - return false; - } - return $file; - } - - /** - * "Pre-registers" our autoloader on the SPL stack. - */ - public static function registerAutoload() - { - $autoload = array('HTMLPurifier_Bootstrap', 'autoload'); - if (($funcs = spl_autoload_functions()) === false) { - spl_autoload_register($autoload); - } elseif (function_exists('spl_autoload_unregister')) { - if (version_compare(PHP_VERSION, '5.3.0', '>=')) { - // prepend flag exists, no need for shenanigans - spl_autoload_register($autoload, true, true); - } else { - $buggy = version_compare(PHP_VERSION, '5.2.11', '<'); - $compat = version_compare(PHP_VERSION, '5.1.2', '<=') && - version_compare(PHP_VERSION, '5.1.0', '>='); - foreach ($funcs as $func) { - if ($buggy && is_array($func)) { - // :TRICKY: There are some compatibility issues and some - // places where we need to error out - $reflector = new ReflectionMethod($func[0], $func[1]); - if (!$reflector->isStatic()) { - throw new Exception( - 'HTML Purifier autoloader registrar is not compatible - with non-static object methods due to PHP Bug #44144; - Please do not use HTMLPurifier.autoload.php (or any - file that includes this file); instead, place the code: - spl_autoload_register(array(\'HTMLPurifier_Bootstrap\', \'autoload\')) - after your own autoloaders.' - ); - } - // Suprisingly, spl_autoload_register supports the - // Class::staticMethod callback format, although call_user_func doesn't - if ($compat) { - $func = implode('::', $func); - } - } - spl_autoload_unregister($func); - } - spl_autoload_register($autoload); - foreach ($funcs as $func) { - spl_autoload_register($func); - } - } - } - } -} - -// vim: et sw=4 sts=4 + +if (!defined('PHP_EOL')) { + switch (strtoupper(substr(PHP_OS, 0, 3))) { + case 'WIN': + define('PHP_EOL', "\r\n"); + break; + case 'DAR': + define('PHP_EOL', "\r"); + break; + default: + define('PHP_EOL', "\n"); + } +} + +/** + * Bootstrap class that contains meta-functionality for HTML Purifier such as + * the autoload function. + * + * @note + * This class may be used without any other files from HTML Purifier. + */ +class HTMLPurifier_Bootstrap +{ + + /** + * Autoload function for HTML Purifier + * @param string $class Class to load + * @return bool + */ + public static function autoload($class) + { + $file = HTMLPurifier_Bootstrap::getPath($class); + if (!$file) { + return false; + } + // Technically speaking, it should be ok and more efficient to + // just do 'require', but Antonio Parraga reports that with + // Zend extensions such as Zend debugger and APC, this invariant + // may be broken. Since we have efficient alternatives, pay + // the cost here and avoid the bug. + require_once HTMLPURIFIER_PREFIX . '/' . $file; + return true; + } + + /** + * Returns the path for a specific class. + * @param string $class Class path to get + * @return string + */ + public static function getPath($class) + { + if (strncmp('HTMLPurifier', $class, 12) !== 0) { + return false; + } + // Custom implementations + if (strncmp('HTMLPurifier_Language_', $class, 22) === 0) { + $code = str_replace('_', '-', substr($class, 22)); + $file = 'HTMLPurifier/Language/classes/' . $code . '.php'; + } else { + $file = str_replace('_', '/', $class) . '.php'; + } + if (!file_exists(HTMLPURIFIER_PREFIX . '/' . $file)) { + return false; + } + return $file; + } + + /** + * "Pre-registers" our autoloader on the SPL stack. + */ + public static function registerAutoload() + { + $autoload = array('HTMLPurifier_Bootstrap', 'autoload'); + if (($funcs = spl_autoload_functions()) === false) { + spl_autoload_register($autoload); + } elseif (function_exists('spl_autoload_unregister')) { + if (version_compare(PHP_VERSION, '5.3.0', '>=')) { + // prepend flag exists, no need for shenanigans + spl_autoload_register($autoload, true, true); + } else { + $buggy = version_compare(PHP_VERSION, '5.2.11', '<'); + $compat = version_compare(PHP_VERSION, '5.1.2', '<=') && + version_compare(PHP_VERSION, '5.1.0', '>='); + foreach ($funcs as $func) { + if ($buggy && is_array($func)) { + // :TRICKY: There are some compatibility issues and some + // places where we need to error out + $reflector = new ReflectionMethod($func[0], $func[1]); + if (!$reflector->isStatic()) { + throw new Exception( + 'HTML Purifier autoloader registrar is not compatible + with non-static object methods due to PHP Bug #44144; + Please do not use HTMLPurifier.autoload.php (or any + file that includes this file); instead, place the code: + spl_autoload_register(array(\'HTMLPurifier_Bootstrap\', \'autoload\')) + after your own autoloaders.' + ); + } + // Suprisingly, spl_autoload_register supports the + // Class::staticMethod callback format, although call_user_func doesn't + if ($compat) { + $func = implode('::', $func); + } + } + spl_autoload_unregister($func); + } + spl_autoload_register($autoload); + foreach ($funcs as $func) { + spl_autoload_register($func); + } + } + } + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/CSSDefinition.php b/lib/purifier/HTMLPurifier/CSSDefinition.php index 5e7490eb2d..21f1a5899d 100644 --- a/lib/purifier/HTMLPurifier/CSSDefinition.php +++ b/lib/purifier/HTMLPurifier/CSSDefinition.php @@ -1,491 +1,533 @@ -info['text-align'] = new HTMLPurifier_AttrDef_Enum( - array('left', 'right', 'center', 'justify'), - false - ); - - $border_style = - $this->info['border-bottom-style'] = - $this->info['border-right-style'] = - $this->info['border-left-style'] = - $this->info['border-top-style'] = new HTMLPurifier_AttrDef_Enum( - array( - 'none', - 'hidden', - 'dotted', - 'dashed', - 'solid', - 'double', - 'groove', - 'ridge', - 'inset', - 'outset' - ), - false - ); - - $this->info['border-style'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_style); - - $this->info['clear'] = new HTMLPurifier_AttrDef_Enum( - array('none', 'left', 'right', 'both'), - false - ); - $this->info['float'] = new HTMLPurifier_AttrDef_Enum( - array('none', 'left', 'right'), - false - ); - $this->info['font-style'] = new HTMLPurifier_AttrDef_Enum( - array('normal', 'italic', 'oblique'), - false - ); - $this->info['font-variant'] = new HTMLPurifier_AttrDef_Enum( - array('normal', 'small-caps'), - false - ); - - $uri_or_none = new HTMLPurifier_AttrDef_CSS_Composite( - array( - new HTMLPurifier_AttrDef_Enum(array('none')), - new HTMLPurifier_AttrDef_CSS_URI() - ) - ); - - $this->info['list-style-position'] = new HTMLPurifier_AttrDef_Enum( - array('inside', 'outside'), - false - ); - $this->info['list-style-type'] = new HTMLPurifier_AttrDef_Enum( - array( - 'disc', - 'circle', - 'square', - 'decimal', - 'lower-roman', - 'upper-roman', - 'lower-alpha', - 'upper-alpha', - 'none' - ), - false - ); - $this->info['list-style-image'] = $uri_or_none; - - $this->info['list-style'] = new HTMLPurifier_AttrDef_CSS_ListStyle($config); - - $this->info['text-transform'] = new HTMLPurifier_AttrDef_Enum( - array('capitalize', 'uppercase', 'lowercase', 'none'), - false - ); - $this->info['color'] = new HTMLPurifier_AttrDef_CSS_Color(); - - $this->info['background-image'] = $uri_or_none; - $this->info['background-repeat'] = new HTMLPurifier_AttrDef_Enum( - array('repeat', 'repeat-x', 'repeat-y', 'no-repeat') - ); - $this->info['background-attachment'] = new HTMLPurifier_AttrDef_Enum( - array('scroll', 'fixed') - ); - $this->info['background-position'] = new HTMLPurifier_AttrDef_CSS_BackgroundPosition(); - - $border_color = - $this->info['border-top-color'] = - $this->info['border-bottom-color'] = - $this->info['border-left-color'] = - $this->info['border-right-color'] = - $this->info['background-color'] = new HTMLPurifier_AttrDef_CSS_Composite( - array( - new HTMLPurifier_AttrDef_Enum(array('transparent')), - new HTMLPurifier_AttrDef_CSS_Color() - ) - ); - - $this->info['background'] = new HTMLPurifier_AttrDef_CSS_Background($config); - - $this->info['border-color'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_color); - - $border_width = - $this->info['border-top-width'] = - $this->info['border-bottom-width'] = - $this->info['border-left-width'] = - $this->info['border-right-width'] = new HTMLPurifier_AttrDef_CSS_Composite( - array( - new HTMLPurifier_AttrDef_Enum(array('thin', 'medium', 'thick')), - new HTMLPurifier_AttrDef_CSS_Length('0') //disallow negative - ) - ); - - $this->info['border-width'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_width); - - $this->info['letter-spacing'] = new HTMLPurifier_AttrDef_CSS_Composite( - array( - new HTMLPurifier_AttrDef_Enum(array('normal')), - new HTMLPurifier_AttrDef_CSS_Length() - ) - ); - - $this->info['word-spacing'] = new HTMLPurifier_AttrDef_CSS_Composite( - array( - new HTMLPurifier_AttrDef_Enum(array('normal')), - new HTMLPurifier_AttrDef_CSS_Length() - ) - ); - - $this->info['font-size'] = new HTMLPurifier_AttrDef_CSS_Composite( - array( - new HTMLPurifier_AttrDef_Enum( - array( - 'xx-small', - 'x-small', - 'small', - 'medium', - 'large', - 'x-large', - 'xx-large', - 'larger', - 'smaller' - ) - ), - new HTMLPurifier_AttrDef_CSS_Percentage(), - new HTMLPurifier_AttrDef_CSS_Length() - ) - ); - - $this->info['line-height'] = new HTMLPurifier_AttrDef_CSS_Composite( - array( - new HTMLPurifier_AttrDef_Enum(array('normal')), - new HTMLPurifier_AttrDef_CSS_Number(true), // no negatives - new HTMLPurifier_AttrDef_CSS_Length('0'), - new HTMLPurifier_AttrDef_CSS_Percentage(true) - ) - ); - - $margin = - $this->info['margin-top'] = - $this->info['margin-bottom'] = - $this->info['margin-left'] = - $this->info['margin-right'] = new HTMLPurifier_AttrDef_CSS_Composite( - array( - new HTMLPurifier_AttrDef_CSS_Length(), - new HTMLPurifier_AttrDef_CSS_Percentage(), - new HTMLPurifier_AttrDef_Enum(array('auto')) - ) - ); - - $this->info['margin'] = new HTMLPurifier_AttrDef_CSS_Multiple($margin); - - // non-negative - $padding = - $this->info['padding-top'] = - $this->info['padding-bottom'] = - $this->info['padding-left'] = - $this->info['padding-right'] = new HTMLPurifier_AttrDef_CSS_Composite( - array( - new HTMLPurifier_AttrDef_CSS_Length('0'), - new HTMLPurifier_AttrDef_CSS_Percentage(true) - ) - ); - - $this->info['padding'] = new HTMLPurifier_AttrDef_CSS_Multiple($padding); - - $this->info['text-indent'] = new HTMLPurifier_AttrDef_CSS_Composite( - array( - new HTMLPurifier_AttrDef_CSS_Length(), - new HTMLPurifier_AttrDef_CSS_Percentage() - ) - ); - - $trusted_wh = new HTMLPurifier_AttrDef_CSS_Composite( - array( - new HTMLPurifier_AttrDef_CSS_Length('0'), - new HTMLPurifier_AttrDef_CSS_Percentage(true), - new HTMLPurifier_AttrDef_Enum(array('auto')) - ) - ); - $max = $config->get('CSS.MaxImgLength'); - - $this->info['min-width'] = - $this->info['max-width'] = - $this->info['min-height'] = - $this->info['max-height'] = - $this->info['width'] = - $this->info['height'] = - $max === null ? - $trusted_wh : - new HTMLPurifier_AttrDef_Switch( - 'img', - // For img tags: - new HTMLPurifier_AttrDef_CSS_Composite( - array( - new HTMLPurifier_AttrDef_CSS_Length('0', $max), - new HTMLPurifier_AttrDef_Enum(array('auto')) - ) - ), - // For everyone else: - $trusted_wh - ); - - $this->info['text-decoration'] = new HTMLPurifier_AttrDef_CSS_TextDecoration(); - - $this->info['font-family'] = new HTMLPurifier_AttrDef_CSS_FontFamily(); - - // this could use specialized code - $this->info['font-weight'] = new HTMLPurifier_AttrDef_Enum( - array( - 'normal', - 'bold', - 'bolder', - 'lighter', - '100', - '200', - '300', - '400', - '500', - '600', - '700', - '800', - '900' - ), - false - ); - - // MUST be called after other font properties, as it references - // a CSSDefinition object - $this->info['font'] = new HTMLPurifier_AttrDef_CSS_Font($config); - - // same here - $this->info['border'] = - $this->info['border-bottom'] = - $this->info['border-top'] = - $this->info['border-left'] = - $this->info['border-right'] = new HTMLPurifier_AttrDef_CSS_Border($config); - - $this->info['border-collapse'] = new HTMLPurifier_AttrDef_Enum( - array('collapse', 'separate') - ); - - $this->info['caption-side'] = new HTMLPurifier_AttrDef_Enum( - array('top', 'bottom') - ); - - $this->info['table-layout'] = new HTMLPurifier_AttrDef_Enum( - array('auto', 'fixed') - ); - - $this->info['vertical-align'] = new HTMLPurifier_AttrDef_CSS_Composite( - array( - new HTMLPurifier_AttrDef_Enum( - array( - 'baseline', - 'sub', - 'super', - 'top', - 'text-top', - 'middle', - 'bottom', - 'text-bottom' - ) - ), - new HTMLPurifier_AttrDef_CSS_Length(), - new HTMLPurifier_AttrDef_CSS_Percentage() - ) - ); - - $this->info['border-spacing'] = new HTMLPurifier_AttrDef_CSS_Multiple(new HTMLPurifier_AttrDef_CSS_Length(), 2); - - // These CSS properties don't work on many browsers, but we live - // in THE FUTURE! - $this->info['white-space'] = new HTMLPurifier_AttrDef_Enum( - array('nowrap', 'normal', 'pre', 'pre-wrap', 'pre-line') - ); - - if ($config->get('CSS.Proprietary')) { - $this->doSetupProprietary($config); - } - - if ($config->get('CSS.AllowTricky')) { - $this->doSetupTricky($config); - } - - if ($config->get('CSS.Trusted')) { - $this->doSetupTrusted($config); - } - - $allow_important = $config->get('CSS.AllowImportant'); - // wrap all attr-defs with decorator that handles !important - foreach ($this->info as $k => $v) { - $this->info[$k] = new HTMLPurifier_AttrDef_CSS_ImportantDecorator($v, $allow_important); - } - - $this->setupConfigStuff($config); - } - - /** - * @param HTMLPurifier_Config $config - */ - protected function doSetupProprietary($config) - { - // Internet Explorer only scrollbar colors - $this->info['scrollbar-arrow-color'] = new HTMLPurifier_AttrDef_CSS_Color(); - $this->info['scrollbar-base-color'] = new HTMLPurifier_AttrDef_CSS_Color(); - $this->info['scrollbar-darkshadow-color'] = new HTMLPurifier_AttrDef_CSS_Color(); - $this->info['scrollbar-face-color'] = new HTMLPurifier_AttrDef_CSS_Color(); - $this->info['scrollbar-highlight-color'] = new HTMLPurifier_AttrDef_CSS_Color(); - $this->info['scrollbar-shadow-color'] = new HTMLPurifier_AttrDef_CSS_Color(); - - // vendor specific prefixes of opacity - $this->info['-moz-opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue(); - $this->info['-khtml-opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue(); - - // only opacity, for now - $this->info['filter'] = new HTMLPurifier_AttrDef_CSS_Filter(); - - // more CSS3 - $this->info['page-break-after'] = - $this->info['page-break-before'] = new HTMLPurifier_AttrDef_Enum( - array( - 'auto', - 'always', - 'avoid', - 'left', - 'right' - ) - ); - $this->info['page-break-inside'] = new HTMLPurifier_AttrDef_Enum(array('auto', 'avoid')); - - $border_radius = new HTMLPurifier_AttrDef_CSS_Composite( - array( - new HTMLPurifier_AttrDef_CSS_Percentage(true), // disallow negative - new HTMLPurifier_AttrDef_CSS_Length('0') // disallow negative - )); - - $this->info['border-top-left-radius'] = - $this->info['border-top-right-radius'] = - $this->info['border-bottom-right-radius'] = - $this->info['border-bottom-left-radius'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_radius, 2); - // TODO: support SLASH syntax - $this->info['border-radius'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_radius, 4); - - } - - /** - * @param HTMLPurifier_Config $config - */ - protected function doSetupTricky($config) - { - $this->info['display'] = new HTMLPurifier_AttrDef_Enum( - array( - 'inline', - 'block', - 'list-item', - 'run-in', - 'compact', - 'marker', - 'table', - 'inline-block', - 'inline-table', - 'table-row-group', - 'table-header-group', - 'table-footer-group', - 'table-row', - 'table-column-group', - 'table-column', - 'table-cell', - 'table-caption', - 'none' - ) - ); - $this->info['visibility'] = new HTMLPurifier_AttrDef_Enum( - array('visible', 'hidden', 'collapse') - ); - $this->info['overflow'] = new HTMLPurifier_AttrDef_Enum(array('visible', 'hidden', 'auto', 'scroll')); - $this->info['opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue(); - } - - /** - * @param HTMLPurifier_Config $config - */ - protected function doSetupTrusted($config) - { - $this->info['position'] = new HTMLPurifier_AttrDef_Enum( - array('static', 'relative', 'absolute', 'fixed') - ); - $this->info['top'] = - $this->info['left'] = - $this->info['right'] = - $this->info['bottom'] = new HTMLPurifier_AttrDef_CSS_Composite( - array( - new HTMLPurifier_AttrDef_CSS_Length(), - new HTMLPurifier_AttrDef_CSS_Percentage(), - new HTMLPurifier_AttrDef_Enum(array('auto')), - ) - ); - $this->info['z-index'] = new HTMLPurifier_AttrDef_CSS_Composite( - array( - new HTMLPurifier_AttrDef_Integer(), - new HTMLPurifier_AttrDef_Enum(array('auto')), - ) - ); - } - - /** - * Performs extra config-based processing. Based off of - * HTMLPurifier_HTMLDefinition. - * @param HTMLPurifier_Config $config - * @todo Refactor duplicate elements into common class (probably using - * composition, not inheritance). - */ - protected function setupConfigStuff($config) - { - // setup allowed elements - $support = "(for information on implementing this, see the " . - "support forums) "; - $allowed_properties = $config->get('CSS.AllowedProperties'); - if ($allowed_properties !== null) { - foreach ($this->info as $name => $d) { - if (!isset($allowed_properties[$name])) { - unset($this->info[$name]); - } - unset($allowed_properties[$name]); - } - // emit errors - foreach ($allowed_properties as $name => $d) { - // :TODO: Is this htmlspecialchars() call really necessary? - $name = htmlspecialchars($name); - trigger_error("Style attribute '$name' is not supported $support", E_USER_WARNING); - } - } - - $forbidden_properties = $config->get('CSS.ForbiddenProperties'); - if ($forbidden_properties !== null) { - foreach ($this->info as $name => $d) { - if (isset($forbidden_properties[$name])) { - unset($this->info[$name]); - } - } - } - } -} - -// vim: et sw=4 sts=4 +info['text-align'] = new HTMLPurifier_AttrDef_Enum( + array('left', 'right', 'center', 'justify'), + false + ); + + $border_style = + $this->info['border-bottom-style'] = + $this->info['border-right-style'] = + $this->info['border-left-style'] = + $this->info['border-top-style'] = new HTMLPurifier_AttrDef_Enum( + array( + 'none', + 'hidden', + 'dotted', + 'dashed', + 'solid', + 'double', + 'groove', + 'ridge', + 'inset', + 'outset' + ), + false + ); + + $this->info['border-style'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_style); + + $this->info['clear'] = new HTMLPurifier_AttrDef_Enum( + array('none', 'left', 'right', 'both'), + false + ); + $this->info['float'] = new HTMLPurifier_AttrDef_Enum( + array('none', 'left', 'right'), + false + ); + $this->info['font-style'] = new HTMLPurifier_AttrDef_Enum( + array('normal', 'italic', 'oblique'), + false + ); + $this->info['font-variant'] = new HTMLPurifier_AttrDef_Enum( + array('normal', 'small-caps'), + false + ); + + $uri_or_none = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum(array('none')), + new HTMLPurifier_AttrDef_CSS_URI() + ) + ); + + $this->info['list-style-position'] = new HTMLPurifier_AttrDef_Enum( + array('inside', 'outside'), + false + ); + $this->info['list-style-type'] = new HTMLPurifier_AttrDef_Enum( + array( + 'disc', + 'circle', + 'square', + 'decimal', + 'lower-roman', + 'upper-roman', + 'lower-alpha', + 'upper-alpha', + 'none' + ), + false + ); + $this->info['list-style-image'] = $uri_or_none; + + $this->info['list-style'] = new HTMLPurifier_AttrDef_CSS_ListStyle($config); + + $this->info['text-transform'] = new HTMLPurifier_AttrDef_Enum( + array('capitalize', 'uppercase', 'lowercase', 'none'), + false + ); + $this->info['color'] = new HTMLPurifier_AttrDef_CSS_Color(); + + $this->info['background-image'] = $uri_or_none; + $this->info['background-repeat'] = new HTMLPurifier_AttrDef_Enum( + array('repeat', 'repeat-x', 'repeat-y', 'no-repeat') + ); + $this->info['background-attachment'] = new HTMLPurifier_AttrDef_Enum( + array('scroll', 'fixed') + ); + $this->info['background-position'] = new HTMLPurifier_AttrDef_CSS_BackgroundPosition(); + + $border_color = + $this->info['border-top-color'] = + $this->info['border-bottom-color'] = + $this->info['border-left-color'] = + $this->info['border-right-color'] = + $this->info['background-color'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum(array('transparent')), + new HTMLPurifier_AttrDef_CSS_Color() + ) + ); + + $this->info['background'] = new HTMLPurifier_AttrDef_CSS_Background($config); + + $this->info['border-color'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_color); + + $border_width = + $this->info['border-top-width'] = + $this->info['border-bottom-width'] = + $this->info['border-left-width'] = + $this->info['border-right-width'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum(array('thin', 'medium', 'thick')), + new HTMLPurifier_AttrDef_CSS_Length('0') //disallow negative + ) + ); + + $this->info['border-width'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_width); + + $this->info['letter-spacing'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum(array('normal')), + new HTMLPurifier_AttrDef_CSS_Length() + ) + ); + + $this->info['word-spacing'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum(array('normal')), + new HTMLPurifier_AttrDef_CSS_Length() + ) + ); + + $this->info['font-size'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum( + array( + 'xx-small', + 'x-small', + 'small', + 'medium', + 'large', + 'x-large', + 'xx-large', + 'larger', + 'smaller' + ) + ), + new HTMLPurifier_AttrDef_CSS_Percentage(), + new HTMLPurifier_AttrDef_CSS_Length() + ) + ); + + $this->info['line-height'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum(array('normal')), + new HTMLPurifier_AttrDef_CSS_Number(true), // no negatives + new HTMLPurifier_AttrDef_CSS_Length('0'), + new HTMLPurifier_AttrDef_CSS_Percentage(true) + ) + ); + + $margin = + $this->info['margin-top'] = + $this->info['margin-bottom'] = + $this->info['margin-left'] = + $this->info['margin-right'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Length(), + new HTMLPurifier_AttrDef_CSS_Percentage(), + new HTMLPurifier_AttrDef_Enum(array('auto')) + ) + ); + + $this->info['margin'] = new HTMLPurifier_AttrDef_CSS_Multiple($margin); + + // non-negative + $padding = + $this->info['padding-top'] = + $this->info['padding-bottom'] = + $this->info['padding-left'] = + $this->info['padding-right'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Length('0'), + new HTMLPurifier_AttrDef_CSS_Percentage(true) + ) + ); + + $this->info['padding'] = new HTMLPurifier_AttrDef_CSS_Multiple($padding); + + $this->info['text-indent'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Length(), + new HTMLPurifier_AttrDef_CSS_Percentage() + ) + ); + + $trusted_wh = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Length('0'), + new HTMLPurifier_AttrDef_CSS_Percentage(true), + new HTMLPurifier_AttrDef_Enum(array('auto', 'initial', 'inherit')) + ) + ); + $trusted_min_wh = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Length('0'), + new HTMLPurifier_AttrDef_CSS_Percentage(true), + new HTMLPurifier_AttrDef_Enum(array('initial', 'inherit')) + ) + ); + $trusted_max_wh = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Length('0'), + new HTMLPurifier_AttrDef_CSS_Percentage(true), + new HTMLPurifier_AttrDef_Enum(array('none', 'initial', 'inherit')) + ) + ); + $max = $config->get('CSS.MaxImgLength'); + + $this->info['width'] = + $this->info['height'] = + $max === null ? + $trusted_wh : + new HTMLPurifier_AttrDef_Switch( + 'img', + // For img tags: + new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Length('0', $max), + new HTMLPurifier_AttrDef_Enum(array('auto')) + ) + ), + // For everyone else: + $trusted_wh + ); + $this->info['min-width'] = + $this->info['min-height'] = + $max === null ? + $trusted_min_wh : + new HTMLPurifier_AttrDef_Switch( + 'img', + // For img tags: + new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Length('0', $max), + new HTMLPurifier_AttrDef_Enum(array('initial', 'inherit')) + ) + ), + // For everyone else: + $trusted_min_wh + ); + $this->info['max-width'] = + $this->info['max-height'] = + $max === null ? + $trusted_max_wh : + new HTMLPurifier_AttrDef_Switch( + 'img', + // For img tags: + new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Length('0', $max), + new HTMLPurifier_AttrDef_Enum(array('none', 'initial', 'inherit')) + ) + ), + // For everyone else: + $trusted_max_wh + ); + + $this->info['text-decoration'] = new HTMLPurifier_AttrDef_CSS_TextDecoration(); + + $this->info['font-family'] = new HTMLPurifier_AttrDef_CSS_FontFamily(); + + // this could use specialized code + $this->info['font-weight'] = new HTMLPurifier_AttrDef_Enum( + array( + 'normal', + 'bold', + 'bolder', + 'lighter', + '100', + '200', + '300', + '400', + '500', + '600', + '700', + '800', + '900' + ), + false + ); + + // MUST be called after other font properties, as it references + // a CSSDefinition object + $this->info['font'] = new HTMLPurifier_AttrDef_CSS_Font($config); + + // same here + $this->info['border'] = + $this->info['border-bottom'] = + $this->info['border-top'] = + $this->info['border-left'] = + $this->info['border-right'] = new HTMLPurifier_AttrDef_CSS_Border($config); + + $this->info['border-collapse'] = new HTMLPurifier_AttrDef_Enum( + array('collapse', 'separate') + ); + + $this->info['caption-side'] = new HTMLPurifier_AttrDef_Enum( + array('top', 'bottom') + ); + + $this->info['table-layout'] = new HTMLPurifier_AttrDef_Enum( + array('auto', 'fixed') + ); + + $this->info['vertical-align'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Enum( + array( + 'baseline', + 'sub', + 'super', + 'top', + 'text-top', + 'middle', + 'bottom', + 'text-bottom' + ) + ), + new HTMLPurifier_AttrDef_CSS_Length(), + new HTMLPurifier_AttrDef_CSS_Percentage() + ) + ); + + $this->info['border-spacing'] = new HTMLPurifier_AttrDef_CSS_Multiple(new HTMLPurifier_AttrDef_CSS_Length(), 2); + + // These CSS properties don't work on many browsers, but we live + // in THE FUTURE! + $this->info['white-space'] = new HTMLPurifier_AttrDef_Enum( + array('nowrap', 'normal', 'pre', 'pre-wrap', 'pre-line') + ); + + if ($config->get('CSS.Proprietary')) { + $this->doSetupProprietary($config); + } + + if ($config->get('CSS.AllowTricky')) { + $this->doSetupTricky($config); + } + + if ($config->get('CSS.Trusted')) { + $this->doSetupTrusted($config); + } + + $allow_important = $config->get('CSS.AllowImportant'); + // wrap all attr-defs with decorator that handles !important + foreach ($this->info as $k => $v) { + $this->info[$k] = new HTMLPurifier_AttrDef_CSS_ImportantDecorator($v, $allow_important); + } + + $this->setupConfigStuff($config); + } + + /** + * @param HTMLPurifier_Config $config + */ + protected function doSetupProprietary($config) + { + // Internet Explorer only scrollbar colors + $this->info['scrollbar-arrow-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + $this->info['scrollbar-base-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + $this->info['scrollbar-darkshadow-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + $this->info['scrollbar-face-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + $this->info['scrollbar-highlight-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + $this->info['scrollbar-shadow-color'] = new HTMLPurifier_AttrDef_CSS_Color(); + + // vendor specific prefixes of opacity + $this->info['-moz-opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue(); + $this->info['-khtml-opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue(); + + // only opacity, for now + $this->info['filter'] = new HTMLPurifier_AttrDef_CSS_Filter(); + + // more CSS3 + $this->info['page-break-after'] = + $this->info['page-break-before'] = new HTMLPurifier_AttrDef_Enum( + array( + 'auto', + 'always', + 'avoid', + 'left', + 'right' + ) + ); + $this->info['page-break-inside'] = new HTMLPurifier_AttrDef_Enum(array('auto', 'avoid')); + + $border_radius = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Percentage(true), // disallow negative + new HTMLPurifier_AttrDef_CSS_Length('0') // disallow negative + )); + + $this->info['border-top-left-radius'] = + $this->info['border-top-right-radius'] = + $this->info['border-bottom-right-radius'] = + $this->info['border-bottom-left-radius'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_radius, 2); + // TODO: support SLASH syntax + $this->info['border-radius'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_radius, 4); + + } + + /** + * @param HTMLPurifier_Config $config + */ + protected function doSetupTricky($config) + { + $this->info['display'] = new HTMLPurifier_AttrDef_Enum( + array( + 'inline', + 'block', + 'list-item', + 'run-in', + 'compact', + 'marker', + 'table', + 'inline-block', + 'inline-table', + 'table-row-group', + 'table-header-group', + 'table-footer-group', + 'table-row', + 'table-column-group', + 'table-column', + 'table-cell', + 'table-caption', + 'none' + ) + ); + $this->info['visibility'] = new HTMLPurifier_AttrDef_Enum( + array('visible', 'hidden', 'collapse') + ); + $this->info['overflow'] = new HTMLPurifier_AttrDef_Enum(array('visible', 'hidden', 'auto', 'scroll')); + $this->info['opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue(); + } + + /** + * @param HTMLPurifier_Config $config + */ + protected function doSetupTrusted($config) + { + $this->info['position'] = new HTMLPurifier_AttrDef_Enum( + array('static', 'relative', 'absolute', 'fixed') + ); + $this->info['top'] = + $this->info['left'] = + $this->info['right'] = + $this->info['bottom'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Length(), + new HTMLPurifier_AttrDef_CSS_Percentage(), + new HTMLPurifier_AttrDef_Enum(array('auto')), + ) + ); + $this->info['z-index'] = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_Integer(), + new HTMLPurifier_AttrDef_Enum(array('auto')), + ) + ); + } + + /** + * Performs extra config-based processing. Based off of + * HTMLPurifier_HTMLDefinition. + * @param HTMLPurifier_Config $config + * @todo Refactor duplicate elements into common class (probably using + * composition, not inheritance). + */ + protected function setupConfigStuff($config) + { + // setup allowed elements + $support = "(for information on implementing this, see the " . + "support forums) "; + $allowed_properties = $config->get('CSS.AllowedProperties'); + if ($allowed_properties !== null) { + foreach ($this->info as $name => $d) { + if (!isset($allowed_properties[$name])) { + unset($this->info[$name]); + } + unset($allowed_properties[$name]); + } + // emit errors + foreach ($allowed_properties as $name => $d) { + // :TODO: Is this htmlspecialchars() call really necessary? + $name = htmlspecialchars($name); + trigger_error("Style attribute '$name' is not supported $support", E_USER_WARNING); + } + } + + $forbidden_properties = $config->get('CSS.ForbiddenProperties'); + if ($forbidden_properties !== null) { + foreach ($this->info as $name => $d) { + if (isset($forbidden_properties[$name])) { + unset($this->info[$name]); + } + } + } + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ChildDef.php b/lib/purifier/HTMLPurifier/ChildDef.php index 5d64cd41a1..8eb17b82e1 100644 --- a/lib/purifier/HTMLPurifier/ChildDef.php +++ b/lib/purifier/HTMLPurifier/ChildDef.php @@ -1,52 +1,52 @@ -elements; - } - - /** - * Validates nodes according to definition and returns modification. - * - * @param HTMLPurifier_Node[] $children Array of HTMLPurifier_Node - * @param HTMLPurifier_Config $config HTMLPurifier_Config object - * @param HTMLPurifier_Context $context HTMLPurifier_Context object - * @return bool|array true to leave nodes as is, false to remove parent node, array of replacement children - */ - abstract public function validateChildren($children, $config, $context); -} - -// vim: et sw=4 sts=4 +elements; + } + + /** + * Validates nodes according to definition and returns modification. + * + * @param HTMLPurifier_Node[] $children Array of HTMLPurifier_Node + * @param HTMLPurifier_Config $config HTMLPurifier_Config object + * @param HTMLPurifier_Context $context HTMLPurifier_Context object + * @return bool|array true to leave nodes as is, false to remove parent node, array of replacement children + */ + abstract public function validateChildren($children, $config, $context); +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ChildDef/Chameleon.php b/lib/purifier/HTMLPurifier/ChildDef/Chameleon.php index f6b2f22e5a..7439be26b6 100644 --- a/lib/purifier/HTMLPurifier/ChildDef/Chameleon.php +++ b/lib/purifier/HTMLPurifier/ChildDef/Chameleon.php @@ -1,67 +1,67 @@ -inline = new HTMLPurifier_ChildDef_Optional($inline); - $this->block = new HTMLPurifier_ChildDef_Optional($block); - $this->elements = $this->block->elements; - } - - /** - * @param HTMLPurifier_Node[] $children - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool - */ - public function validateChildren($children, $config, $context) - { - if ($context->get('IsInline') === false) { - return $this->block->validateChildren( - $children, - $config, - $context - ); - } else { - return $this->inline->validateChildren( - $children, - $config, - $context - ); - } - } -} - -// vim: et sw=4 sts=4 +inline = new HTMLPurifier_ChildDef_Optional($inline); + $this->block = new HTMLPurifier_ChildDef_Optional($block); + $this->elements = $this->block->elements; + } + + /** + * @param HTMLPurifier_Node[] $children + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function validateChildren($children, $config, $context) + { + if ($context->get('IsInline') === false) { + return $this->block->validateChildren( + $children, + $config, + $context + ); + } else { + return $this->inline->validateChildren( + $children, + $config, + $context + ); + } + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ChildDef/Custom.php b/lib/purifier/HTMLPurifier/ChildDef/Custom.php index cc84d89908..f515888a1d 100644 --- a/lib/purifier/HTMLPurifier/ChildDef/Custom.php +++ b/lib/purifier/HTMLPurifier/ChildDef/Custom.php @@ -1,102 +1,102 @@ -dtd_regex = $dtd_regex; - $this->_compileRegex(); - } - - /** - * Compiles the PCRE regex from a DTD regex ($dtd_regex to $_pcre_regex) - */ - protected function _compileRegex() - { - $raw = str_replace(' ', '', $this->dtd_regex); - if ($raw[0] != '(') { - $raw = "($raw)"; - } - $el = '[#a-zA-Z0-9_.-]+'; - $reg = $raw; - - // COMPLICATED! AND MIGHT BE BUGGY! I HAVE NO CLUE WHAT I'M - // DOING! Seriously: if there's problems, please report them. - - // collect all elements into the $elements array - preg_match_all("/$el/", $reg, $matches); - foreach ($matches[0] as $match) { - $this->elements[$match] = true; - } - - // setup all elements as parentheticals with leading commas - $reg = preg_replace("/$el/", '(,\\0)', $reg); - - // remove commas when they were not solicited - $reg = preg_replace("/([^,(|]\(+),/", '\\1', $reg); - - // remove all non-paranthetical commas: they are handled by first regex - $reg = preg_replace("/,\(/", '(', $reg); - - $this->_pcre_regex = $reg; - } - - /** - * @param HTMLPurifier_Node[] $children - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return bool - */ - public function validateChildren($children, $config, $context) - { - $list_of_children = ''; - $nesting = 0; // depth into the nest - foreach ($children as $node) { - if (!empty($node->is_whitespace)) { - continue; - } - $list_of_children .= $node->name . ','; - } - // add leading comma to deal with stray comma declarations - $list_of_children = ',' . rtrim($list_of_children, ','); - $okay = - preg_match( - '/^,?' . $this->_pcre_regex . '$/', - $list_of_children - ); - return (bool)$okay; - } -} - -// vim: et sw=4 sts=4 +dtd_regex = $dtd_regex; + $this->_compileRegex(); + } + + /** + * Compiles the PCRE regex from a DTD regex ($dtd_regex to $_pcre_regex) + */ + protected function _compileRegex() + { + $raw = str_replace(' ', '', $this->dtd_regex); + if ($raw[0] != '(') { + $raw = "($raw)"; + } + $el = '[#a-zA-Z0-9_.-]+'; + $reg = $raw; + + // COMPLICATED! AND MIGHT BE BUGGY! I HAVE NO CLUE WHAT I'M + // DOING! Seriously: if there's problems, please report them. + + // collect all elements into the $elements array + preg_match_all("/$el/", $reg, $matches); + foreach ($matches[0] as $match) { + $this->elements[$match] = true; + } + + // setup all elements as parentheticals with leading commas + $reg = preg_replace("/$el/", '(,\\0)', $reg); + + // remove commas when they were not solicited + $reg = preg_replace("/([^,(|]\(+),/", '\\1', $reg); + + // remove all non-paranthetical commas: they are handled by first regex + $reg = preg_replace("/,\(/", '(', $reg); + + $this->_pcre_regex = $reg; + } + + /** + * @param HTMLPurifier_Node[] $children + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return bool + */ + public function validateChildren($children, $config, $context) + { + $list_of_children = ''; + $nesting = 0; // depth into the nest + foreach ($children as $node) { + if (!empty($node->is_whitespace)) { + continue; + } + $list_of_children .= $node->name . ','; + } + // add leading comma to deal with stray comma declarations + $list_of_children = ',' . rtrim($list_of_children, ','); + $okay = + preg_match( + '/^,?' . $this->_pcre_regex . '$/', + $list_of_children + ); + return (bool)$okay; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ChildDef/Empty.php b/lib/purifier/HTMLPurifier/ChildDef/Empty.php index bbcde56e8f..a8a6cbdd2c 100644 --- a/lib/purifier/HTMLPurifier/ChildDef/Empty.php +++ b/lib/purifier/HTMLPurifier/ChildDef/Empty.php @@ -1,38 +1,38 @@ - true, 'ul' => true, 'ol' => true); - - /** - * @param array $children - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return array - */ - public function validateChildren($children, $config, $context) - { - // Flag for subclasses - $this->whitespace = false; - - // if there are no tokens, delete parent node - if (empty($children)) { - return false; - } - - // if li is not allowed, delete parent node - if (!isset($config->getHTMLDefinition()->info['li'])) { - trigger_error("Cannot allow ul/ol without allowing li", E_USER_WARNING); - return false; - } - - // the new set of children - $result = array(); - - // a little sanity check to make sure it's not ALL whitespace - $all_whitespace = true; - - $current_li = null; - - foreach ($children as $node) { - if (!empty($node->is_whitespace)) { - $result[] = $node; - continue; - } - $all_whitespace = false; // phew, we're not talking about whitespace - - if ($node->name === 'li') { - // good - $current_li = $node; - $result[] = $node; - } else { - // we want to tuck this into the previous li - // Invariant: we expect the node to be ol/ul - // ToDo: Make this more robust in the case of not ol/ul - // by distinguishing between existing li and li created - // to handle non-list elements; non-list elements should - // not be appended to an existing li; only li created - // for non-list. This distinction is not currently made. - if ($current_li === null) { - $current_li = new HTMLPurifier_Node_Element('li'); - $result[] = $current_li; - } - $current_li->children[] = $node; - $current_li->empty = false; // XXX fascinating! Check for this error elsewhere ToDo - } - } - if (empty($result)) { - return false; - } - if ($all_whitespace) { - return false; - } - return $result; - } -} - -// vim: et sw=4 sts=4 + true, 'ul' => true, 'ol' => true); + + /** + * @param array $children + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function validateChildren($children, $config, $context) + { + // Flag for subclasses + $this->whitespace = false; + + // if there are no tokens, delete parent node + if (empty($children)) { + return false; + } + + // if li is not allowed, delete parent node + if (!isset($config->getHTMLDefinition()->info['li'])) { + trigger_error("Cannot allow ul/ol without allowing li", E_USER_WARNING); + return false; + } + + // the new set of children + $result = array(); + + // a little sanity check to make sure it's not ALL whitespace + $all_whitespace = true; + + $current_li = null; + + foreach ($children as $node) { + if (!empty($node->is_whitespace)) { + $result[] = $node; + continue; + } + $all_whitespace = false; // phew, we're not talking about whitespace + + if ($node->name === 'li') { + // good + $current_li = $node; + $result[] = $node; + } else { + // we want to tuck this into the previous li + // Invariant: we expect the node to be ol/ul + // ToDo: Make this more robust in the case of not ol/ul + // by distinguishing between existing li and li created + // to handle non-list elements; non-list elements should + // not be appended to an existing li; only li created + // for non-list. This distinction is not currently made. + if ($current_li === null) { + $current_li = new HTMLPurifier_Node_Element('li'); + $result[] = $current_li; + } + $current_li->children[] = $node; + $current_li->empty = false; // XXX fascinating! Check for this error elsewhere ToDo + } + } + if (empty($result)) { + return false; + } + if ($all_whitespace) { + return false; + } + return $result; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ChildDef/Optional.php b/lib/purifier/HTMLPurifier/ChildDef/Optional.php index 1db864d907..b9468063b1 100644 --- a/lib/purifier/HTMLPurifier/ChildDef/Optional.php +++ b/lib/purifier/HTMLPurifier/ChildDef/Optional.php @@ -1,45 +1,45 @@ -whitespace) { - return $children; - } else { - return array(); - } - } - return $result; - } -} - -// vim: et sw=4 sts=4 +whitespace) { + return $children; + } else { + return array(); + } + } + return $result; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ChildDef/Required.php b/lib/purifier/HTMLPurifier/ChildDef/Required.php index f6b8e8a2f0..0d1c8f5f39 100644 --- a/lib/purifier/HTMLPurifier/ChildDef/Required.php +++ b/lib/purifier/HTMLPurifier/ChildDef/Required.php @@ -1,118 +1,118 @@ - $x) { - $elements[$i] = true; - if (empty($i)) { - unset($elements[$i]); - } // remove blank - } - } - $this->elements = $elements; - } - - /** - * @type bool - */ - public $allow_empty = false; - - /** - * @type string - */ - public $type = 'required'; - - /** - * @param array $children - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return array - */ - public function validateChildren($children, $config, $context) - { - // Flag for subclasses - $this->whitespace = false; - - // if there are no tokens, delete parent node - if (empty($children)) { - return false; - } - - // the new set of children - $result = array(); - - // whether or not parsed character data is allowed - // this controls whether or not we silently drop a tag - // or generate escaped HTML from it - $pcdata_allowed = isset($this->elements['#PCDATA']); - - // a little sanity check to make sure it's not ALL whitespace - $all_whitespace = true; - - $stack = array_reverse($children); - while (!empty($stack)) { - $node = array_pop($stack); - if (!empty($node->is_whitespace)) { - $result[] = $node; - continue; - } - $all_whitespace = false; // phew, we're not talking about whitespace - - if (!isset($this->elements[$node->name])) { - // special case text - // XXX One of these ought to be redundant or something - if ($pcdata_allowed && $node instanceof HTMLPurifier_Node_Text) { - $result[] = $node; - continue; - } - // spill the child contents in - // ToDo: Make configurable - if ($node instanceof HTMLPurifier_Node_Element) { - for ($i = count($node->children) - 1; $i >= 0; $i--) { - $stack[] = $node->children[$i]; - } - continue; - } - continue; - } - $result[] = $node; - } - if (empty($result)) { - return false; - } - if ($all_whitespace) { - $this->whitespace = true; - return false; - } - return $result; - } -} - -// vim: et sw=4 sts=4 + $x) { + $elements[$i] = true; + if (empty($i)) { + unset($elements[$i]); + } // remove blank + } + } + $this->elements = $elements; + } + + /** + * @type bool + */ + public $allow_empty = false; + + /** + * @type string + */ + public $type = 'required'; + + /** + * @param array $children + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function validateChildren($children, $config, $context) + { + // Flag for subclasses + $this->whitespace = false; + + // if there are no tokens, delete parent node + if (empty($children)) { + return false; + } + + // the new set of children + $result = array(); + + // whether or not parsed character data is allowed + // this controls whether or not we silently drop a tag + // or generate escaped HTML from it + $pcdata_allowed = isset($this->elements['#PCDATA']); + + // a little sanity check to make sure it's not ALL whitespace + $all_whitespace = true; + + $stack = array_reverse($children); + while (!empty($stack)) { + $node = array_pop($stack); + if (!empty($node->is_whitespace)) { + $result[] = $node; + continue; + } + $all_whitespace = false; // phew, we're not talking about whitespace + + if (!isset($this->elements[$node->name])) { + // special case text + // XXX One of these ought to be redundant or something + if ($pcdata_allowed && $node instanceof HTMLPurifier_Node_Text) { + $result[] = $node; + continue; + } + // spill the child contents in + // ToDo: Make configurable + if ($node instanceof HTMLPurifier_Node_Element) { + for ($i = count($node->children) - 1; $i >= 0; $i--) { + $stack[] = $node->children[$i]; + } + continue; + } + continue; + } + $result[] = $node; + } + if (empty($result)) { + return false; + } + if ($all_whitespace) { + $this->whitespace = true; + return false; + } + return $result; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ChildDef/StrictBlockquote.php b/lib/purifier/HTMLPurifier/ChildDef/StrictBlockquote.php index 38bf9533eb..3270a46e1b 100644 --- a/lib/purifier/HTMLPurifier/ChildDef/StrictBlockquote.php +++ b/lib/purifier/HTMLPurifier/ChildDef/StrictBlockquote.php @@ -1,110 +1,110 @@ -init($config); - return $this->fake_elements; - } - - /** - * @param array $children - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return array - */ - public function validateChildren($children, $config, $context) - { - $this->init($config); - - // trick the parent class into thinking it allows more - $this->elements = $this->fake_elements; - $result = parent::validateChildren($children, $config, $context); - $this->elements = $this->real_elements; - - if ($result === false) { - return array(); - } - if ($result === true) { - $result = $children; - } - - $def = $config->getHTMLDefinition(); - $block_wrap_name = $def->info_block_wrapper; - $block_wrap = false; - $ret = array(); - - foreach ($result as $node) { - if ($block_wrap === false) { - if (($node instanceof HTMLPurifier_Node_Text && !$node->is_whitespace) || - ($node instanceof HTMLPurifier_Node_Element && !isset($this->elements[$node->name]))) { - $block_wrap = new HTMLPurifier_Node_Element($def->info_block_wrapper); - $ret[] = $block_wrap; - } - } else { - if ($node instanceof HTMLPurifier_Node_Element && isset($this->elements[$node->name])) { - $block_wrap = false; - - } - } - if ($block_wrap) { - $block_wrap->children[] = $node; - } else { - $ret[] = $node; - } - } - return $ret; - } - - /** - * @param HTMLPurifier_Config $config - */ - private function init($config) - { - if (!$this->init) { - $def = $config->getHTMLDefinition(); - // allow all inline elements - $this->real_elements = $this->elements; - $this->fake_elements = $def->info_content_sets['Flow']; - $this->fake_elements['#PCDATA'] = true; - $this->init = true; - } - } -} - -// vim: et sw=4 sts=4 +init($config); + return $this->fake_elements; + } + + /** + * @param array $children + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function validateChildren($children, $config, $context) + { + $this->init($config); + + // trick the parent class into thinking it allows more + $this->elements = $this->fake_elements; + $result = parent::validateChildren($children, $config, $context); + $this->elements = $this->real_elements; + + if ($result === false) { + return array(); + } + if ($result === true) { + $result = $children; + } + + $def = $config->getHTMLDefinition(); + $block_wrap_name = $def->info_block_wrapper; + $block_wrap = false; + $ret = array(); + + foreach ($result as $node) { + if ($block_wrap === false) { + if (($node instanceof HTMLPurifier_Node_Text && !$node->is_whitespace) || + ($node instanceof HTMLPurifier_Node_Element && !isset($this->elements[$node->name]))) { + $block_wrap = new HTMLPurifier_Node_Element($def->info_block_wrapper); + $ret[] = $block_wrap; + } + } else { + if ($node instanceof HTMLPurifier_Node_Element && isset($this->elements[$node->name])) { + $block_wrap = false; + + } + } + if ($block_wrap) { + $block_wrap->children[] = $node; + } else { + $ret[] = $node; + } + } + return $ret; + } + + /** + * @param HTMLPurifier_Config $config + */ + private function init($config) + { + if (!$this->init) { + $def = $config->getHTMLDefinition(); + // allow all inline elements + $this->real_elements = $this->elements; + $this->fake_elements = $def->info_content_sets['Flow']; + $this->fake_elements['#PCDATA'] = true; + $this->init = true; + } + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ChildDef/Table.php b/lib/purifier/HTMLPurifier/ChildDef/Table.php index 9b12c92803..cb6b3e6cdc 100644 --- a/lib/purifier/HTMLPurifier/ChildDef/Table.php +++ b/lib/purifier/HTMLPurifier/ChildDef/Table.php @@ -1,224 +1,224 @@ - true, - 'tbody' => true, - 'thead' => true, - 'tfoot' => true, - 'caption' => true, - 'colgroup' => true, - 'col' => true - ); - - public function __construct() - { - } - - /** - * @param array $children - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return array - */ - public function validateChildren($children, $config, $context) - { - if (empty($children)) { - return false; - } - - // only one of these elements is allowed in a table - $caption = false; - $thead = false; - $tfoot = false; - - // whitespace - $initial_ws = array(); - $after_caption_ws = array(); - $after_thead_ws = array(); - $after_tfoot_ws = array(); - - // as many of these as you want - $cols = array(); - $content = array(); - - $tbody_mode = false; // if true, then we need to wrap any stray - // s with a . - - $ws_accum =& $initial_ws; - - foreach ($children as $node) { - if ($node instanceof HTMLPurifier_Node_Comment) { - $ws_accum[] = $node; - continue; - } - switch ($node->name) { - case 'tbody': - $tbody_mode = true; - // fall through - case 'tr': - $content[] = $node; - $ws_accum =& $content; - break; - case 'caption': - // there can only be one caption! - if ($caption !== false) break; - $caption = $node; - $ws_accum =& $after_caption_ws; - break; - case 'thead': - $tbody_mode = true; - // XXX This breaks rendering properties with - // Firefox, which never floats a to - // the top. Ever. (Our scheme will float the - // first to the top.) So maybe - // s that are not first should be - // turned into ? Very tricky, indeed. - if ($thead === false) { - $thead = $node; - $ws_accum =& $after_thead_ws; - } else { - // Oops, there's a second one! What - // should we do? Current behavior is to - // transmutate the first and last entries into - // tbody tags, and then put into content. - // Maybe a better idea is to *attach - // it* to the existing thead or tfoot? - // We don't do this, because Firefox - // doesn't float an extra tfoot to the - // bottom like it does for the first one. - $node->name = 'tbody'; - $content[] = $node; - $ws_accum =& $content; - } - break; - case 'tfoot': - // see above for some aveats - $tbody_mode = true; - if ($tfoot === false) { - $tfoot = $node; - $ws_accum =& $after_tfoot_ws; - } else { - $node->name = 'tbody'; - $content[] = $node; - $ws_accum =& $content; - } - break; - case 'colgroup': - case 'col': - $cols[] = $node; - $ws_accum =& $cols; - break; - case '#PCDATA': - // How is whitespace handled? We treat is as sticky to - // the *end* of the previous element. So all of the - // nonsense we have worked on is to keep things - // together. - if (!empty($node->is_whitespace)) { - $ws_accum[] = $node; - } - break; - } - } - - if (empty($content)) { - return false; - } - - $ret = $initial_ws; - if ($caption !== false) { - $ret[] = $caption; - $ret = array_merge($ret, $after_caption_ws); - } - if ($cols !== false) { - $ret = array_merge($ret, $cols); - } - if ($thead !== false) { - $ret[] = $thead; - $ret = array_merge($ret, $after_thead_ws); - } - if ($tfoot !== false) { - $ret[] = $tfoot; - $ret = array_merge($ret, $after_tfoot_ws); - } - - if ($tbody_mode) { - // we have to shuffle tr into tbody - $current_tr_tbody = null; - - foreach($content as $node) { - switch ($node->name) { - case 'tbody': - $current_tr_tbody = null; - $ret[] = $node; - break; - case 'tr': - if ($current_tr_tbody === null) { - $current_tr_tbody = new HTMLPurifier_Node_Element('tbody'); - $ret[] = $current_tr_tbody; - } - $current_tr_tbody->children[] = $node; - break; - case '#PCDATA': - //assert($node->is_whitespace); - if ($current_tr_tbody === null) { - $ret[] = $node; - } else { - $current_tr_tbody->children[] = $node; - } - break; - } - } - } else { - $ret = array_merge($ret, $content); - } - - return $ret; - - } -} - -// vim: et sw=4 sts=4 + true, + 'tbody' => true, + 'thead' => true, + 'tfoot' => true, + 'caption' => true, + 'colgroup' => true, + 'col' => true + ); + + public function __construct() + { + } + + /** + * @param array $children + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array + */ + public function validateChildren($children, $config, $context) + { + if (empty($children)) { + return false; + } + + // only one of these elements is allowed in a table + $caption = false; + $thead = false; + $tfoot = false; + + // whitespace + $initial_ws = array(); + $after_caption_ws = array(); + $after_thead_ws = array(); + $after_tfoot_ws = array(); + + // as many of these as you want + $cols = array(); + $content = array(); + + $tbody_mode = false; // if true, then we need to wrap any stray + // s with a . + + $ws_accum =& $initial_ws; + + foreach ($children as $node) { + if ($node instanceof HTMLPurifier_Node_Comment) { + $ws_accum[] = $node; + continue; + } + switch ($node->name) { + case 'tbody': + $tbody_mode = true; + // fall through + case 'tr': + $content[] = $node; + $ws_accum =& $content; + break; + case 'caption': + // there can only be one caption! + if ($caption !== false) break; + $caption = $node; + $ws_accum =& $after_caption_ws; + break; + case 'thead': + $tbody_mode = true; + // XXX This breaks rendering properties with + // Firefox, which never floats a to + // the top. Ever. (Our scheme will float the + // first to the top.) So maybe + // s that are not first should be + // turned into ? Very tricky, indeed. + if ($thead === false) { + $thead = $node; + $ws_accum =& $after_thead_ws; + } else { + // Oops, there's a second one! What + // should we do? Current behavior is to + // transmutate the first and last entries into + // tbody tags, and then put into content. + // Maybe a better idea is to *attach + // it* to the existing thead or tfoot? + // We don't do this, because Firefox + // doesn't float an extra tfoot to the + // bottom like it does for the first one. + $node->name = 'tbody'; + $content[] = $node; + $ws_accum =& $content; + } + break; + case 'tfoot': + // see above for some aveats + $tbody_mode = true; + if ($tfoot === false) { + $tfoot = $node; + $ws_accum =& $after_tfoot_ws; + } else { + $node->name = 'tbody'; + $content[] = $node; + $ws_accum =& $content; + } + break; + case 'colgroup': + case 'col': + $cols[] = $node; + $ws_accum =& $cols; + break; + case '#PCDATA': + // How is whitespace handled? We treat is as sticky to + // the *end* of the previous element. So all of the + // nonsense we have worked on is to keep things + // together. + if (!empty($node->is_whitespace)) { + $ws_accum[] = $node; + } + break; + } + } + + if (empty($content)) { + return false; + } + + $ret = $initial_ws; + if ($caption !== false) { + $ret[] = $caption; + $ret = array_merge($ret, $after_caption_ws); + } + if ($cols !== false) { + $ret = array_merge($ret, $cols); + } + if ($thead !== false) { + $ret[] = $thead; + $ret = array_merge($ret, $after_thead_ws); + } + if ($tfoot !== false) { + $ret[] = $tfoot; + $ret = array_merge($ret, $after_tfoot_ws); + } + + if ($tbody_mode) { + // we have to shuffle tr into tbody + $current_tr_tbody = null; + + foreach($content as $node) { + switch ($node->name) { + case 'tbody': + $current_tr_tbody = null; + $ret[] = $node; + break; + case 'tr': + if ($current_tr_tbody === null) { + $current_tr_tbody = new HTMLPurifier_Node_Element('tbody'); + $ret[] = $current_tr_tbody; + } + $current_tr_tbody->children[] = $node; + break; + case '#PCDATA': + //assert($node->is_whitespace); + if ($current_tr_tbody === null) { + $ret[] = $node; + } else { + $current_tr_tbody->children[] = $node; + } + break; + } + } + } else { + $ret = array_merge($ret, $content); + } + + return $ret; + + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/Config.php b/lib/purifier/HTMLPurifier/Config.php index e6ca53ae0e..3133d8a4ff 100644 --- a/lib/purifier/HTMLPurifier/Config.php +++ b/lib/purifier/HTMLPurifier/Config.php @@ -1,920 +1,920 @@ -defaultPlist; - $this->plist = new HTMLPurifier_PropertyList($parent); - $this->def = $definition; // keep a copy around for checking - $this->parser = new HTMLPurifier_VarParser_Flexible(); - } - - /** - * Convenience constructor that creates a config object based on a mixed var - * @param mixed $config Variable that defines the state of the config - * object. Can be: a HTMLPurifier_Config() object, - * an array of directives based on loadArray(), - * or a string filename of an ini file. - * @param HTMLPurifier_ConfigSchema $schema Schema object - * @return HTMLPurifier_Config Configured object - */ - public static function create($config, $schema = null) - { - if ($config instanceof HTMLPurifier_Config) { - // pass-through - return $config; - } - if (!$schema) { - $ret = HTMLPurifier_Config::createDefault(); - } else { - $ret = new HTMLPurifier_Config($schema); - } - if (is_string($config)) { - $ret->loadIni($config); - } elseif (is_array($config)) $ret->loadArray($config); - return $ret; - } - - /** - * Creates a new config object that inherits from a previous one. - * @param HTMLPurifier_Config $config Configuration object to inherit from. - * @return HTMLPurifier_Config object with $config as its parent. - */ - public static function inherit(HTMLPurifier_Config $config) - { - return new HTMLPurifier_Config($config->def, $config->plist); - } - - /** - * Convenience constructor that creates a default configuration object. - * @return HTMLPurifier_Config default object. - */ - public static function createDefault() - { - $definition = HTMLPurifier_ConfigSchema::instance(); - $config = new HTMLPurifier_Config($definition); - return $config; - } - - /** - * Retrieves a value from the configuration. - * - * @param string $key String key - * @param mixed $a - * - * @return mixed - */ - public function get($key, $a = null) - { - if ($a !== null) { - $this->triggerError( - "Using deprecated API: use \$config->get('$key.$a') instead", - E_USER_WARNING - ); - $key = "$key.$a"; - } - if (!$this->finalized) { - $this->autoFinalize(); - } - if (!isset($this->def->info[$key])) { - // can't add % due to SimpleTest bug - $this->triggerError( - 'Cannot retrieve value of undefined directive ' . htmlspecialchars($key), - E_USER_WARNING - ); - return; - } - if (isset($this->def->info[$key]->isAlias)) { - $d = $this->def->info[$key]; - $this->triggerError( - 'Cannot get value from aliased directive, use real name ' . $d->key, - E_USER_ERROR - ); - return; - } - if ($this->lock) { - list($ns) = explode('.', $key); - if ($ns !== $this->lock) { - $this->triggerError( - 'Cannot get value of namespace ' . $ns . ' when lock for ' . - $this->lock . - ' is active, this probably indicates a Definition setup method ' . - 'is accessing directives that are not within its namespace', - E_USER_ERROR - ); - return; - } - } - return $this->plist->get($key); - } - - /** - * Retrieves an array of directives to values from a given namespace - * - * @param string $namespace String namespace - * - * @return array - */ - public function getBatch($namespace) - { - if (!$this->finalized) { - $this->autoFinalize(); - } - $full = $this->getAll(); - if (!isset($full[$namespace])) { - $this->triggerError( - 'Cannot retrieve undefined namespace ' . - htmlspecialchars($namespace), - E_USER_WARNING - ); - return; - } - return $full[$namespace]; - } - - /** - * Returns a SHA-1 signature of a segment of the configuration object - * that uniquely identifies that particular configuration - * - * @param string $namespace Namespace to get serial for - * - * @return string - * @note Revision is handled specially and is removed from the batch - * before processing! - */ - public function getBatchSerial($namespace) - { - if (empty($this->serials[$namespace])) { - $batch = $this->getBatch($namespace); - unset($batch['DefinitionRev']); - $this->serials[$namespace] = sha1(serialize($batch)); - } - return $this->serials[$namespace]; - } - - /** - * Returns a SHA-1 signature for the entire configuration object - * that uniquely identifies that particular configuration - * - * @return string - */ - public function getSerial() - { - if (empty($this->serial)) { - $this->serial = sha1(serialize($this->getAll())); - } - return $this->serial; - } - - /** - * Retrieves all directives, organized by namespace - * - * @warning This is a pretty inefficient function, avoid if you can - */ - public function getAll() - { - if (!$this->finalized) { - $this->autoFinalize(); - } - $ret = array(); - foreach ($this->plist->squash() as $name => $value) { - list($ns, $key) = explode('.', $name, 2); - $ret[$ns][$key] = $value; - } - return $ret; - } - - /** - * Sets a value to configuration. - * - * @param string $key key - * @param mixed $value value - * @param mixed $a - */ - public function set($key, $value, $a = null) - { - if (strpos($key, '.') === false) { - $namespace = $key; - $directive = $value; - $value = $a; - $key = "$key.$directive"; - $this->triggerError("Using deprecated API: use \$config->set('$key', ...) instead", E_USER_NOTICE); - } else { - list($namespace) = explode('.', $key); - } - if ($this->isFinalized('Cannot set directive after finalization')) { - return; - } - if (!isset($this->def->info[$key])) { - $this->triggerError( - 'Cannot set undefined directive ' . htmlspecialchars($key) . ' to value', - E_USER_WARNING - ); - return; - } - $def = $this->def->info[$key]; - - if (isset($def->isAlias)) { - if ($this->aliasMode) { - $this->triggerError( - 'Double-aliases not allowed, please fix '. - 'ConfigSchema bug with' . $key, - E_USER_ERROR - ); - return; - } - $this->aliasMode = true; - $this->set($def->key, $value); - $this->aliasMode = false; - $this->triggerError("$key is an alias, preferred directive name is {$def->key}", E_USER_NOTICE); - return; - } - - // Raw type might be negative when using the fully optimized form - // of stdClass, which indicates allow_null == true - $rtype = is_int($def) ? $def : $def->type; - if ($rtype < 0) { - $type = -$rtype; - $allow_null = true; - } else { - $type = $rtype; - $allow_null = isset($def->allow_null); - } - - try { - $value = $this->parser->parse($value, $type, $allow_null); - } catch (HTMLPurifier_VarParserException $e) { - $this->triggerError( - 'Value for ' . $key . ' is of invalid type, should be ' . - HTMLPurifier_VarParser::getTypeName($type), - E_USER_WARNING - ); - return; - } - if (is_string($value) && is_object($def)) { - // resolve value alias if defined - if (isset($def->aliases[$value])) { - $value = $def->aliases[$value]; - } - // check to see if the value is allowed - if (isset($def->allowed) && !isset($def->allowed[$value])) { - $this->triggerError( - 'Value not supported, valid values are: ' . - $this->_listify($def->allowed), - E_USER_WARNING - ); - return; - } - } - $this->plist->set($key, $value); - - // reset definitions if the directives they depend on changed - // this is a very costly process, so it's discouraged - // with finalization - if ($namespace == 'HTML' || $namespace == 'CSS' || $namespace == 'URI') { - $this->definitions[$namespace] = null; - } - - $this->serials[$namespace] = false; - } - - /** - * Convenience function for error reporting - * - * @param array $lookup - * - * @return string - */ - private function _listify($lookup) - { - $list = array(); - foreach ($lookup as $name => $b) { - $list[] = $name; - } - return implode(', ', $list); - } - - /** - * Retrieves object reference to the HTML definition. - * - * @param bool $raw Return a copy that has not been setup yet. Must be - * called before it's been setup, otherwise won't work. - * @param bool $optimized If true, this method may return null, to - * indicate that a cached version of the modified - * definition object is available and no further edits - * are necessary. Consider using - * maybeGetRawHTMLDefinition, which is more explicitly - * named, instead. - * - * @return HTMLPurifier_HTMLDefinition - */ - public function getHTMLDefinition($raw = false, $optimized = false) - { - return $this->getDefinition('HTML', $raw, $optimized); - } - - /** - * Retrieves object reference to the CSS definition - * - * @param bool $raw Return a copy that has not been setup yet. Must be - * called before it's been setup, otherwise won't work. - * @param bool $optimized If true, this method may return null, to - * indicate that a cached version of the modified - * definition object is available and no further edits - * are necessary. Consider using - * maybeGetRawCSSDefinition, which is more explicitly - * named, instead. - * - * @return HTMLPurifier_CSSDefinition - */ - public function getCSSDefinition($raw = false, $optimized = false) - { - return $this->getDefinition('CSS', $raw, $optimized); - } - - /** - * Retrieves object reference to the URI definition - * - * @param bool $raw Return a copy that has not been setup yet. Must be - * called before it's been setup, otherwise won't work. - * @param bool $optimized If true, this method may return null, to - * indicate that a cached version of the modified - * definition object is available and no further edits - * are necessary. Consider using - * maybeGetRawURIDefinition, which is more explicitly - * named, instead. - * - * @return HTMLPurifier_URIDefinition - */ - public function getURIDefinition($raw = false, $optimized = false) - { - return $this->getDefinition('URI', $raw, $optimized); - } - - /** - * Retrieves a definition - * - * @param string $type Type of definition: HTML, CSS, etc - * @param bool $raw Whether or not definition should be returned raw - * @param bool $optimized Only has an effect when $raw is true. Whether - * or not to return null if the result is already present in - * the cache. This is off by default for backwards - * compatibility reasons, but you need to do things this - * way in order to ensure that caching is done properly. - * Check out enduser-customize.html for more details. - * We probably won't ever change this default, as much as the - * maybe semantics is the "right thing to do." - * - * @throws HTMLPurifier_Exception - * @return HTMLPurifier_Definition - */ - public function getDefinition($type, $raw = false, $optimized = false) - { - if ($optimized && !$raw) { - throw new HTMLPurifier_Exception("Cannot set optimized = true when raw = false"); - } - if (!$this->finalized) { - $this->autoFinalize(); - } - // temporarily suspend locks, so we can handle recursive definition calls - $lock = $this->lock; - $this->lock = null; - $factory = HTMLPurifier_DefinitionCacheFactory::instance(); - $cache = $factory->create($type, $this); - $this->lock = $lock; - if (!$raw) { - // full definition - // --------------- - // check if definition is in memory - if (!empty($this->definitions[$type])) { - $def = $this->definitions[$type]; - // check if the definition is setup - if ($def->setup) { - return $def; - } else { - $def->setup($this); - if ($def->optimized) { - $cache->add($def, $this); - } - return $def; - } - } - // check if definition is in cache - $def = $cache->get($this); - if ($def) { - // definition in cache, save to memory and return it - $this->definitions[$type] = $def; - return $def; - } - // initialize it - $def = $this->initDefinition($type); - // set it up - $this->lock = $type; - $def->setup($this); - $this->lock = null; - // save in cache - $cache->add($def, $this); - // return it - return $def; - } else { - // raw definition - // -------------- - // check preconditions - $def = null; - if ($optimized) { - if (is_null($this->get($type . '.DefinitionID'))) { - // fatally error out if definition ID not set - throw new HTMLPurifier_Exception( - "Cannot retrieve raw version without specifying %$type.DefinitionID" - ); - } - } - if (!empty($this->definitions[$type])) { - $def = $this->definitions[$type]; - if ($def->setup && !$optimized) { - $extra = $this->chatty ? - " (try moving this code block earlier in your initialization)" : - ""; - throw new HTMLPurifier_Exception( - "Cannot retrieve raw definition after it has already been setup" . - $extra - ); - } - if ($def->optimized === null) { - $extra = $this->chatty ? " (try flushing your cache)" : ""; - throw new HTMLPurifier_Exception( - "Optimization status of definition is unknown" . $extra - ); - } - if ($def->optimized !== $optimized) { - $msg = $optimized ? "optimized" : "unoptimized"; - $extra = $this->chatty ? - " (this backtrace is for the first inconsistent call, which was for a $msg raw definition)" - : ""; - throw new HTMLPurifier_Exception( - "Inconsistent use of optimized and unoptimized raw definition retrievals" . $extra - ); - } - } - // check if definition was in memory - if ($def) { - if ($def->setup) { - // invariant: $optimized === true (checked above) - return null; - } else { - return $def; - } - } - // if optimized, check if definition was in cache - // (because we do the memory check first, this formulation - // is prone to cache slamming, but I think - // guaranteeing that either /all/ of the raw - // setup code or /none/ of it is run is more important.) - if ($optimized) { - // This code path only gets run once; once we put - // something in $definitions (which is guaranteed by the - // trailing code), we always short-circuit above. - $def = $cache->get($this); - if ($def) { - // save the full definition for later, but don't - // return it yet - $this->definitions[$type] = $def; - return null; - } - } - // check invariants for creation - if (!$optimized) { - if (!is_null($this->get($type . '.DefinitionID'))) { - if ($this->chatty) { - $this->triggerError( - 'Due to a documentation error in previous version of HTML Purifier, your ' . - 'definitions are not being cached. If this is OK, you can remove the ' . - '%$type.DefinitionRev and %$type.DefinitionID declaration. Otherwise, ' . - 'modify your code to use maybeGetRawDefinition, and test if the returned ' . - 'value is null before making any edits (if it is null, that means that a ' . - 'cached version is available, and no raw operations are necessary). See ' . - '' . - 'Customize for more details', - E_USER_WARNING - ); - } else { - $this->triggerError( - "Useless DefinitionID declaration", - E_USER_WARNING - ); - } - } - } - // initialize it - $def = $this->initDefinition($type); - $def->optimized = $optimized; - return $def; - } - throw new HTMLPurifier_Exception("The impossible happened!"); - } - - /** - * Initialise definition - * - * @param string $type What type of definition to create - * - * @return HTMLPurifier_CSSDefinition|HTMLPurifier_HTMLDefinition|HTMLPurifier_URIDefinition - * @throws HTMLPurifier_Exception - */ - private function initDefinition($type) - { - // quick checks failed, let's create the object - if ($type == 'HTML') { - $def = new HTMLPurifier_HTMLDefinition(); - } elseif ($type == 'CSS') { - $def = new HTMLPurifier_CSSDefinition(); - } elseif ($type == 'URI') { - $def = new HTMLPurifier_URIDefinition(); - } else { - throw new HTMLPurifier_Exception( - "Definition of $type type not supported" - ); - } - $this->definitions[$type] = $def; - return $def; - } - - public function maybeGetRawDefinition($name) - { - return $this->getDefinition($name, true, true); - } - - /** - * @return HTMLPurifier_HTMLDefinition - */ - public function maybeGetRawHTMLDefinition() - { - return $this->getDefinition('HTML', true, true); - } - - /** - * @return HTMLPurifier_CSSDefinition - */ - public function maybeGetRawCSSDefinition() - { - return $this->getDefinition('CSS', true, true); - } - - /** - * @return HTMLPurifier_URIDefinition - */ - public function maybeGetRawURIDefinition() - { - return $this->getDefinition('URI', true, true); - } - - /** - * Loads configuration values from an array with the following structure: - * Namespace.Directive => Value - * - * @param array $config_array Configuration associative array - */ - public function loadArray($config_array) - { - if ($this->isFinalized('Cannot load directives after finalization')) { - return; - } - foreach ($config_array as $key => $value) { - $key = str_replace('_', '.', $key); - if (strpos($key, '.') !== false) { - $this->set($key, $value); - } else { - $namespace = $key; - $namespace_values = $value; - foreach ($namespace_values as $directive => $value2) { - $this->set($namespace .'.'. $directive, $value2); - } - } - } - } - - /** - * Returns a list of array(namespace, directive) for all directives - * that are allowed in a web-form context as per an allowed - * namespaces/directives list. - * - * @param array $allowed List of allowed namespaces/directives - * @param HTMLPurifier_ConfigSchema $schema Schema to use, if not global copy - * - * @return array - */ - public static function getAllowedDirectivesForForm($allowed, $schema = null) - { - if (!$schema) { - $schema = HTMLPurifier_ConfigSchema::instance(); - } - if ($allowed !== true) { - if (is_string($allowed)) { - $allowed = array($allowed); - } - $allowed_ns = array(); - $allowed_directives = array(); - $blacklisted_directives = array(); - foreach ($allowed as $ns_or_directive) { - if (strpos($ns_or_directive, '.') !== false) { - // directive - if ($ns_or_directive[0] == '-') { - $blacklisted_directives[substr($ns_or_directive, 1)] = true; - } else { - $allowed_directives[$ns_or_directive] = true; - } - } else { - // namespace - $allowed_ns[$ns_or_directive] = true; - } - } - } - $ret = array(); - foreach ($schema->info as $key => $def) { - list($ns, $directive) = explode('.', $key, 2); - if ($allowed !== true) { - if (isset($blacklisted_directives["$ns.$directive"])) { - continue; - } - if (!isset($allowed_directives["$ns.$directive"]) && !isset($allowed_ns[$ns])) { - continue; - } - } - if (isset($def->isAlias)) { - continue; - } - if ($directive == 'DefinitionID' || $directive == 'DefinitionRev') { - continue; - } - $ret[] = array($ns, $directive); - } - return $ret; - } - - /** - * Loads configuration values from $_GET/$_POST that were posted - * via ConfigForm - * - * @param array $array $_GET or $_POST array to import - * @param string|bool $index Index/name that the config variables are in - * @param array|bool $allowed List of allowed namespaces/directives - * @param bool $mq_fix Boolean whether or not to enable magic quotes fix - * @param HTMLPurifier_ConfigSchema $schema Schema to use, if not global copy - * - * @return mixed - */ - public static function loadArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true, $schema = null) - { - $ret = HTMLPurifier_Config::prepareArrayFromForm($array, $index, $allowed, $mq_fix, $schema); - $config = HTMLPurifier_Config::create($ret, $schema); - return $config; - } - - /** - * Merges in configuration values from $_GET/$_POST to object. NOT STATIC. - * - * @param array $array $_GET or $_POST array to import - * @param string|bool $index Index/name that the config variables are in - * @param array|bool $allowed List of allowed namespaces/directives - * @param bool $mq_fix Boolean whether or not to enable magic quotes fix - */ - public function mergeArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true) - { - $ret = HTMLPurifier_Config::prepareArrayFromForm($array, $index, $allowed, $mq_fix, $this->def); - $this->loadArray($ret); - } - - /** - * Prepares an array from a form into something usable for the more - * strict parts of HTMLPurifier_Config - * - * @param array $array $_GET or $_POST array to import - * @param string|bool $index Index/name that the config variables are in - * @param array|bool $allowed List of allowed namespaces/directives - * @param bool $mq_fix Boolean whether or not to enable magic quotes fix - * @param HTMLPurifier_ConfigSchema $schema Schema to use, if not global copy - * - * @return array - */ - public static function prepareArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true, $schema = null) - { - if ($index !== false) { - $array = (isset($array[$index]) && is_array($array[$index])) ? $array[$index] : array(); - } - $mq = $mq_fix && function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc(); - - $allowed = HTMLPurifier_Config::getAllowedDirectivesForForm($allowed, $schema); - $ret = array(); - foreach ($allowed as $key) { - list($ns, $directive) = $key; - $skey = "$ns.$directive"; - if (!empty($array["Null_$skey"])) { - $ret[$ns][$directive] = null; - continue; - } - if (!isset($array[$skey])) { - continue; - } - $value = $mq ? stripslashes($array[$skey]) : $array[$skey]; - $ret[$ns][$directive] = $value; - } - return $ret; - } - - /** - * Loads configuration values from an ini file - * - * @param string $filename Name of ini file - */ - public function loadIni($filename) - { - if ($this->isFinalized('Cannot load directives after finalization')) { - return; - } - $array = parse_ini_file($filename, true); - $this->loadArray($array); - } - - /** - * Checks whether or not the configuration object is finalized. - * - * @param string|bool $error String error message, or false for no error - * - * @return bool - */ - public function isFinalized($error = false) - { - if ($this->finalized && $error) { - $this->triggerError($error, E_USER_ERROR); - } - return $this->finalized; - } - - /** - * Finalizes configuration only if auto finalize is on and not - * already finalized - */ - public function autoFinalize() - { - if ($this->autoFinalize) { - $this->finalize(); - } else { - $this->plist->squash(true); - } - } - - /** - * Finalizes a configuration object, prohibiting further change - */ - public function finalize() - { - $this->finalized = true; - $this->parser = null; - } - - /** - * Produces a nicely formatted error message by supplying the - * stack frame information OUTSIDE of HTMLPurifier_Config. - * - * @param string $msg An error message - * @param int $no An error number - */ - protected function triggerError($msg, $no) - { - // determine previous stack frame - $extra = ''; - if ($this->chatty) { - $trace = debug_backtrace(); - // zip(tail(trace), trace) -- but PHP is not Haskell har har - for ($i = 0, $c = count($trace); $i < $c - 1; $i++) { - // XXX this is not correct on some versions of HTML Purifier - if ($trace[$i + 1]['class'] === 'HTMLPurifier_Config') { - continue; - } - $frame = $trace[$i]; - $extra = " invoked on line {$frame['line']} in file {$frame['file']}"; - break; - } - } - trigger_error($msg . $extra, $no); - } - - /** - * Returns a serialized form of the configuration object that can - * be reconstituted. - * - * @return string - */ - public function serialize() - { - $this->getDefinition('HTML'); - $this->getDefinition('CSS'); - $this->getDefinition('URI'); - return serialize($this); - } - -} - -// vim: et sw=4 sts=4 +defaultPlist; + $this->plist = new HTMLPurifier_PropertyList($parent); + $this->def = $definition; // keep a copy around for checking + $this->parser = new HTMLPurifier_VarParser_Flexible(); + } + + /** + * Convenience constructor that creates a config object based on a mixed var + * @param mixed $config Variable that defines the state of the config + * object. Can be: a HTMLPurifier_Config() object, + * an array of directives based on loadArray(), + * or a string filename of an ini file. + * @param HTMLPurifier_ConfigSchema $schema Schema object + * @return HTMLPurifier_Config Configured object + */ + public static function create($config, $schema = null) + { + if ($config instanceof HTMLPurifier_Config) { + // pass-through + return $config; + } + if (!$schema) { + $ret = HTMLPurifier_Config::createDefault(); + } else { + $ret = new HTMLPurifier_Config($schema); + } + if (is_string($config)) { + $ret->loadIni($config); + } elseif (is_array($config)) $ret->loadArray($config); + return $ret; + } + + /** + * Creates a new config object that inherits from a previous one. + * @param HTMLPurifier_Config $config Configuration object to inherit from. + * @return HTMLPurifier_Config object with $config as its parent. + */ + public static function inherit(HTMLPurifier_Config $config) + { + return new HTMLPurifier_Config($config->def, $config->plist); + } + + /** + * Convenience constructor that creates a default configuration object. + * @return HTMLPurifier_Config default object. + */ + public static function createDefault() + { + $definition = HTMLPurifier_ConfigSchema::instance(); + $config = new HTMLPurifier_Config($definition); + return $config; + } + + /** + * Retrieves a value from the configuration. + * + * @param string $key String key + * @param mixed $a + * + * @return mixed + */ + public function get($key, $a = null) + { + if ($a !== null) { + $this->triggerError( + "Using deprecated API: use \$config->get('$key.$a') instead", + E_USER_WARNING + ); + $key = "$key.$a"; + } + if (!$this->finalized) { + $this->autoFinalize(); + } + if (!isset($this->def->info[$key])) { + // can't add % due to SimpleTest bug + $this->triggerError( + 'Cannot retrieve value of undefined directive ' . htmlspecialchars($key), + E_USER_WARNING + ); + return; + } + if (isset($this->def->info[$key]->isAlias)) { + $d = $this->def->info[$key]; + $this->triggerError( + 'Cannot get value from aliased directive, use real name ' . $d->key, + E_USER_ERROR + ); + return; + } + if ($this->lock) { + list($ns) = explode('.', $key); + if ($ns !== $this->lock) { + $this->triggerError( + 'Cannot get value of namespace ' . $ns . ' when lock for ' . + $this->lock . + ' is active, this probably indicates a Definition setup method ' . + 'is accessing directives that are not within its namespace', + E_USER_ERROR + ); + return; + } + } + return $this->plist->get($key); + } + + /** + * Retrieves an array of directives to values from a given namespace + * + * @param string $namespace String namespace + * + * @return array + */ + public function getBatch($namespace) + { + if (!$this->finalized) { + $this->autoFinalize(); + } + $full = $this->getAll(); + if (!isset($full[$namespace])) { + $this->triggerError( + 'Cannot retrieve undefined namespace ' . + htmlspecialchars($namespace), + E_USER_WARNING + ); + return; + } + return $full[$namespace]; + } + + /** + * Returns a SHA-1 signature of a segment of the configuration object + * that uniquely identifies that particular configuration + * + * @param string $namespace Namespace to get serial for + * + * @return string + * @note Revision is handled specially and is removed from the batch + * before processing! + */ + public function getBatchSerial($namespace) + { + if (empty($this->serials[$namespace])) { + $batch = $this->getBatch($namespace); + unset($batch['DefinitionRev']); + $this->serials[$namespace] = sha1(serialize($batch)); + } + return $this->serials[$namespace]; + } + + /** + * Returns a SHA-1 signature for the entire configuration object + * that uniquely identifies that particular configuration + * + * @return string + */ + public function getSerial() + { + if (empty($this->serial)) { + $this->serial = sha1(serialize($this->getAll())); + } + return $this->serial; + } + + /** + * Retrieves all directives, organized by namespace + * + * @warning This is a pretty inefficient function, avoid if you can + */ + public function getAll() + { + if (!$this->finalized) { + $this->autoFinalize(); + } + $ret = array(); + foreach ($this->plist->squash() as $name => $value) { + list($ns, $key) = explode('.', $name, 2); + $ret[$ns][$key] = $value; + } + return $ret; + } + + /** + * Sets a value to configuration. + * + * @param string $key key + * @param mixed $value value + * @param mixed $a + */ + public function set($key, $value, $a = null) + { + if (strpos($key, '.') === false) { + $namespace = $key; + $directive = $value; + $value = $a; + $key = "$key.$directive"; + $this->triggerError("Using deprecated API: use \$config->set('$key', ...) instead", E_USER_NOTICE); + } else { + list($namespace) = explode('.', $key); + } + if ($this->isFinalized('Cannot set directive after finalization')) { + return; + } + if (!isset($this->def->info[$key])) { + $this->triggerError( + 'Cannot set undefined directive ' . htmlspecialchars($key) . ' to value', + E_USER_WARNING + ); + return; + } + $def = $this->def->info[$key]; + + if (isset($def->isAlias)) { + if ($this->aliasMode) { + $this->triggerError( + 'Double-aliases not allowed, please fix '. + 'ConfigSchema bug with' . $key, + E_USER_ERROR + ); + return; + } + $this->aliasMode = true; + $this->set($def->key, $value); + $this->aliasMode = false; + $this->triggerError("$key is an alias, preferred directive name is {$def->key}", E_USER_NOTICE); + return; + } + + // Raw type might be negative when using the fully optimized form + // of stdClass, which indicates allow_null == true + $rtype = is_int($def) ? $def : $def->type; + if ($rtype < 0) { + $type = -$rtype; + $allow_null = true; + } else { + $type = $rtype; + $allow_null = isset($def->allow_null); + } + + try { + $value = $this->parser->parse($value, $type, $allow_null); + } catch (HTMLPurifier_VarParserException $e) { + $this->triggerError( + 'Value for ' . $key . ' is of invalid type, should be ' . + HTMLPurifier_VarParser::getTypeName($type), + E_USER_WARNING + ); + return; + } + if (is_string($value) && is_object($def)) { + // resolve value alias if defined + if (isset($def->aliases[$value])) { + $value = $def->aliases[$value]; + } + // check to see if the value is allowed + if (isset($def->allowed) && !isset($def->allowed[$value])) { + $this->triggerError( + 'Value not supported, valid values are: ' . + $this->_listify($def->allowed), + E_USER_WARNING + ); + return; + } + } + $this->plist->set($key, $value); + + // reset definitions if the directives they depend on changed + // this is a very costly process, so it's discouraged + // with finalization + if ($namespace == 'HTML' || $namespace == 'CSS' || $namespace == 'URI') { + $this->definitions[$namespace] = null; + } + + $this->serials[$namespace] = false; + } + + /** + * Convenience function for error reporting + * + * @param array $lookup + * + * @return string + */ + private function _listify($lookup) + { + $list = array(); + foreach ($lookup as $name => $b) { + $list[] = $name; + } + return implode(', ', $list); + } + + /** + * Retrieves object reference to the HTML definition. + * + * @param bool $raw Return a copy that has not been setup yet. Must be + * called before it's been setup, otherwise won't work. + * @param bool $optimized If true, this method may return null, to + * indicate that a cached version of the modified + * definition object is available and no further edits + * are necessary. Consider using + * maybeGetRawHTMLDefinition, which is more explicitly + * named, instead. + * + * @return HTMLPurifier_HTMLDefinition|null + */ + public function getHTMLDefinition($raw = false, $optimized = false) + { + return $this->getDefinition('HTML', $raw, $optimized); + } + + /** + * Retrieves object reference to the CSS definition + * + * @param bool $raw Return a copy that has not been setup yet. Must be + * called before it's been setup, otherwise won't work. + * @param bool $optimized If true, this method may return null, to + * indicate that a cached version of the modified + * definition object is available and no further edits + * are necessary. Consider using + * maybeGetRawCSSDefinition, which is more explicitly + * named, instead. + * + * @return HTMLPurifier_CSSDefinition|null + */ + public function getCSSDefinition($raw = false, $optimized = false) + { + return $this->getDefinition('CSS', $raw, $optimized); + } + + /** + * Retrieves object reference to the URI definition + * + * @param bool $raw Return a copy that has not been setup yet. Must be + * called before it's been setup, otherwise won't work. + * @param bool $optimized If true, this method may return null, to + * indicate that a cached version of the modified + * definition object is available and no further edits + * are necessary. Consider using + * maybeGetRawURIDefinition, which is more explicitly + * named, instead. + * + * @return HTMLPurifier_URIDefinition|null + */ + public function getURIDefinition($raw = false, $optimized = false) + { + return $this->getDefinition('URI', $raw, $optimized); + } + + /** + * Retrieves a definition + * + * @param string $type Type of definition: HTML, CSS, etc + * @param bool $raw Whether or not definition should be returned raw + * @param bool $optimized Only has an effect when $raw is true. Whether + * or not to return null if the result is already present in + * the cache. This is off by default for backwards + * compatibility reasons, but you need to do things this + * way in order to ensure that caching is done properly. + * Check out enduser-customize.html for more details. + * We probably won't ever change this default, as much as the + * maybe semantics is the "right thing to do." + * + * @throws HTMLPurifier_Exception + * @return HTMLPurifier_Definition|null + */ + public function getDefinition($type, $raw = false, $optimized = false) + { + if ($optimized && !$raw) { + throw new HTMLPurifier_Exception("Cannot set optimized = true when raw = false"); + } + if (!$this->finalized) { + $this->autoFinalize(); + } + // temporarily suspend locks, so we can handle recursive definition calls + $lock = $this->lock; + $this->lock = null; + $factory = HTMLPurifier_DefinitionCacheFactory::instance(); + $cache = $factory->create($type, $this); + $this->lock = $lock; + if (!$raw) { + // full definition + // --------------- + // check if definition is in memory + if (!empty($this->definitions[$type])) { + $def = $this->definitions[$type]; + // check if the definition is setup + if ($def->setup) { + return $def; + } else { + $def->setup($this); + if ($def->optimized) { + $cache->add($def, $this); + } + return $def; + } + } + // check if definition is in cache + $def = $cache->get($this); + if ($def) { + // definition in cache, save to memory and return it + $this->definitions[$type] = $def; + return $def; + } + // initialize it + $def = $this->initDefinition($type); + // set it up + $this->lock = $type; + $def->setup($this); + $this->lock = null; + // save in cache + $cache->add($def, $this); + // return it + return $def; + } else { + // raw definition + // -------------- + // check preconditions + $def = null; + if ($optimized) { + if (is_null($this->get($type . '.DefinitionID'))) { + // fatally error out if definition ID not set + throw new HTMLPurifier_Exception( + "Cannot retrieve raw version without specifying %$type.DefinitionID" + ); + } + } + if (!empty($this->definitions[$type])) { + $def = $this->definitions[$type]; + if ($def->setup && !$optimized) { + $extra = $this->chatty ? + " (try moving this code block earlier in your initialization)" : + ""; + throw new HTMLPurifier_Exception( + "Cannot retrieve raw definition after it has already been setup" . + $extra + ); + } + if ($def->optimized === null) { + $extra = $this->chatty ? " (try flushing your cache)" : ""; + throw new HTMLPurifier_Exception( + "Optimization status of definition is unknown" . $extra + ); + } + if ($def->optimized !== $optimized) { + $msg = $optimized ? "optimized" : "unoptimized"; + $extra = $this->chatty ? + " (this backtrace is for the first inconsistent call, which was for a $msg raw definition)" + : ""; + throw new HTMLPurifier_Exception( + "Inconsistent use of optimized and unoptimized raw definition retrievals" . $extra + ); + } + } + // check if definition was in memory + if ($def) { + if ($def->setup) { + // invariant: $optimized === true (checked above) + return null; + } else { + return $def; + } + } + // if optimized, check if definition was in cache + // (because we do the memory check first, this formulation + // is prone to cache slamming, but I think + // guaranteeing that either /all/ of the raw + // setup code or /none/ of it is run is more important.) + if ($optimized) { + // This code path only gets run once; once we put + // something in $definitions (which is guaranteed by the + // trailing code), we always short-circuit above. + $def = $cache->get($this); + if ($def) { + // save the full definition for later, but don't + // return it yet + $this->definitions[$type] = $def; + return null; + } + } + // check invariants for creation + if (!$optimized) { + if (!is_null($this->get($type . '.DefinitionID'))) { + if ($this->chatty) { + $this->triggerError( + 'Due to a documentation error in previous version of HTML Purifier, your ' . + 'definitions are not being cached. If this is OK, you can remove the ' . + '%$type.DefinitionRev and %$type.DefinitionID declaration. Otherwise, ' . + 'modify your code to use maybeGetRawDefinition, and test if the returned ' . + 'value is null before making any edits (if it is null, that means that a ' . + 'cached version is available, and no raw operations are necessary). See ' . + '' . + 'Customize for more details', + E_USER_WARNING + ); + } else { + $this->triggerError( + "Useless DefinitionID declaration", + E_USER_WARNING + ); + } + } + } + // initialize it + $def = $this->initDefinition($type); + $def->optimized = $optimized; + return $def; + } + throw new HTMLPurifier_Exception("The impossible happened!"); + } + + /** + * Initialise definition + * + * @param string $type What type of definition to create + * + * @return HTMLPurifier_CSSDefinition|HTMLPurifier_HTMLDefinition|HTMLPurifier_URIDefinition + * @throws HTMLPurifier_Exception + */ + private function initDefinition($type) + { + // quick checks failed, let's create the object + if ($type == 'HTML') { + $def = new HTMLPurifier_HTMLDefinition(); + } elseif ($type == 'CSS') { + $def = new HTMLPurifier_CSSDefinition(); + } elseif ($type == 'URI') { + $def = new HTMLPurifier_URIDefinition(); + } else { + throw new HTMLPurifier_Exception( + "Definition of $type type not supported" + ); + } + $this->definitions[$type] = $def; + return $def; + } + + public function maybeGetRawDefinition($name) + { + return $this->getDefinition($name, true, true); + } + + /** + * @return HTMLPurifier_HTMLDefinition|null + */ + public function maybeGetRawHTMLDefinition() + { + return $this->getDefinition('HTML', true, true); + } + + /** + * @return HTMLPurifier_CSSDefinition|null + */ + public function maybeGetRawCSSDefinition() + { + return $this->getDefinition('CSS', true, true); + } + + /** + * @return HTMLPurifier_URIDefinition|null + */ + public function maybeGetRawURIDefinition() + { + return $this->getDefinition('URI', true, true); + } + + /** + * Loads configuration values from an array with the following structure: + * Namespace.Directive => Value + * + * @param array $config_array Configuration associative array + */ + public function loadArray($config_array) + { + if ($this->isFinalized('Cannot load directives after finalization')) { + return; + } + foreach ($config_array as $key => $value) { + $key = str_replace('_', '.', $key); + if (strpos($key, '.') !== false) { + $this->set($key, $value); + } else { + $namespace = $key; + $namespace_values = $value; + foreach ($namespace_values as $directive => $value2) { + $this->set($namespace .'.'. $directive, $value2); + } + } + } + } + + /** + * Returns a list of array(namespace, directive) for all directives + * that are allowed in a web-form context as per an allowed + * namespaces/directives list. + * + * @param array $allowed List of allowed namespaces/directives + * @param HTMLPurifier_ConfigSchema $schema Schema to use, if not global copy + * + * @return array + */ + public static function getAllowedDirectivesForForm($allowed, $schema = null) + { + if (!$schema) { + $schema = HTMLPurifier_ConfigSchema::instance(); + } + if ($allowed !== true) { + if (is_string($allowed)) { + $allowed = array($allowed); + } + $allowed_ns = array(); + $allowed_directives = array(); + $blacklisted_directives = array(); + foreach ($allowed as $ns_or_directive) { + if (strpos($ns_or_directive, '.') !== false) { + // directive + if ($ns_or_directive[0] == '-') { + $blacklisted_directives[substr($ns_or_directive, 1)] = true; + } else { + $allowed_directives[$ns_or_directive] = true; + } + } else { + // namespace + $allowed_ns[$ns_or_directive] = true; + } + } + } + $ret = array(); + foreach ($schema->info as $key => $def) { + list($ns, $directive) = explode('.', $key, 2); + if ($allowed !== true) { + if (isset($blacklisted_directives["$ns.$directive"])) { + continue; + } + if (!isset($allowed_directives["$ns.$directive"]) && !isset($allowed_ns[$ns])) { + continue; + } + } + if (isset($def->isAlias)) { + continue; + } + if ($directive == 'DefinitionID' || $directive == 'DefinitionRev') { + continue; + } + $ret[] = array($ns, $directive); + } + return $ret; + } + + /** + * Loads configuration values from $_GET/$_POST that were posted + * via ConfigForm + * + * @param array $array $_GET or $_POST array to import + * @param string|bool $index Index/name that the config variables are in + * @param array|bool $allowed List of allowed namespaces/directives + * @param bool $mq_fix Boolean whether or not to enable magic quotes fix + * @param HTMLPurifier_ConfigSchema $schema Schema to use, if not global copy + * + * @return mixed + */ + public static function loadArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true, $schema = null) + { + $ret = HTMLPurifier_Config::prepareArrayFromForm($array, $index, $allowed, $mq_fix, $schema); + $config = HTMLPurifier_Config::create($ret, $schema); + return $config; + } + + /** + * Merges in configuration values from $_GET/$_POST to object. NOT STATIC. + * + * @param array $array $_GET or $_POST array to import + * @param string|bool $index Index/name that the config variables are in + * @param array|bool $allowed List of allowed namespaces/directives + * @param bool $mq_fix Boolean whether or not to enable magic quotes fix + */ + public function mergeArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true) + { + $ret = HTMLPurifier_Config::prepareArrayFromForm($array, $index, $allowed, $mq_fix, $this->def); + $this->loadArray($ret); + } + + /** + * Prepares an array from a form into something usable for the more + * strict parts of HTMLPurifier_Config + * + * @param array $array $_GET or $_POST array to import + * @param string|bool $index Index/name that the config variables are in + * @param array|bool $allowed List of allowed namespaces/directives + * @param bool $mq_fix Boolean whether or not to enable magic quotes fix + * @param HTMLPurifier_ConfigSchema $schema Schema to use, if not global copy + * + * @return array + */ + public static function prepareArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true, $schema = null) + { + if ($index !== false) { + $array = (isset($array[$index]) && is_array($array[$index])) ? $array[$index] : array(); + } + $mq = $mq_fix && function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc(); + + $allowed = HTMLPurifier_Config::getAllowedDirectivesForForm($allowed, $schema); + $ret = array(); + foreach ($allowed as $key) { + list($ns, $directive) = $key; + $skey = "$ns.$directive"; + if (!empty($array["Null_$skey"])) { + $ret[$ns][$directive] = null; + continue; + } + if (!isset($array[$skey])) { + continue; + } + $value = $mq ? stripslashes($array[$skey]) : $array[$skey]; + $ret[$ns][$directive] = $value; + } + return $ret; + } + + /** + * Loads configuration values from an ini file + * + * @param string $filename Name of ini file + */ + public function loadIni($filename) + { + if ($this->isFinalized('Cannot load directives after finalization')) { + return; + } + $array = parse_ini_file($filename, true); + $this->loadArray($array); + } + + /** + * Checks whether or not the configuration object is finalized. + * + * @param string|bool $error String error message, or false for no error + * + * @return bool + */ + public function isFinalized($error = false) + { + if ($this->finalized && $error) { + $this->triggerError($error, E_USER_ERROR); + } + return $this->finalized; + } + + /** + * Finalizes configuration only if auto finalize is on and not + * already finalized + */ + public function autoFinalize() + { + if ($this->autoFinalize) { + $this->finalize(); + } else { + $this->plist->squash(true); + } + } + + /** + * Finalizes a configuration object, prohibiting further change + */ + public function finalize() + { + $this->finalized = true; + $this->parser = null; + } + + /** + * Produces a nicely formatted error message by supplying the + * stack frame information OUTSIDE of HTMLPurifier_Config. + * + * @param string $msg An error message + * @param int $no An error number + */ + protected function triggerError($msg, $no) + { + // determine previous stack frame + $extra = ''; + if ($this->chatty) { + $trace = debug_backtrace(); + // zip(tail(trace), trace) -- but PHP is not Haskell har har + for ($i = 0, $c = count($trace); $i < $c - 1; $i++) { + // XXX this is not correct on some versions of HTML Purifier + if (isset($trace[$i + 1]['class']) && $trace[$i + 1]['class'] === 'HTMLPurifier_Config') { + continue; + } + $frame = $trace[$i]; + $extra = " invoked on line {$frame['line']} in file {$frame['file']}"; + break; + } + } + trigger_error($msg . $extra, $no); + } + + /** + * Returns a serialized form of the configuration object that can + * be reconstituted. + * + * @return string + */ + public function serialize() + { + $this->getDefinition('HTML'); + $this->getDefinition('CSS'); + $this->getDefinition('URI'); + return serialize($this); + } + +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema.php b/lib/purifier/HTMLPurifier/ConfigSchema.php index cc8f471666..c3fe8cd4ab 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema.php +++ b/lib/purifier/HTMLPurifier/ConfigSchema.php @@ -1,176 +1,176 @@ - array( - * 'Directive' => new stdClass(), - * ) - * ) - * - * The stdClass may have the following properties: - * - * - If isAlias isn't set: - * - type: Integer type of directive, see HTMLPurifier_VarParser for definitions - * - allow_null: If set, this directive allows null values - * - aliases: If set, an associative array of value aliases to real values - * - allowed: If set, a lookup array of allowed (string) values - * - If isAlias is set: - * - namespace: Namespace this directive aliases to - * - name: Directive name this directive aliases to - * - * In certain degenerate cases, stdClass will actually be an integer. In - * that case, the value is equivalent to an stdClass with the type - * property set to the integer. If the integer is negative, type is - * equal to the absolute value of integer, and allow_null is true. - * - * This class is friendly with HTMLPurifier_Config. If you need introspection - * about the schema, you're better of using the ConfigSchema_Interchange, - * which uses more memory but has much richer information. - * @type array - */ - public $info = array(); - - /** - * Application-wide singleton - * @type HTMLPurifier_ConfigSchema - */ - protected static $singleton; - - public function __construct() - { - $this->defaultPlist = new HTMLPurifier_PropertyList(); - } - - /** - * Unserializes the default ConfigSchema. - * @return HTMLPurifier_ConfigSchema - */ - public static function makeFromSerial() - { - $contents = file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/ConfigSchema/schema.ser'); - $r = unserialize($contents); - if (!$r) { - $hash = sha1($contents); - trigger_error("Unserialization of configuration schema failed, sha1 of file was $hash", E_USER_ERROR); - } - return $r; - } - - /** - * Retrieves an instance of the application-wide configuration definition. - * @param HTMLPurifier_ConfigSchema $prototype - * @return HTMLPurifier_ConfigSchema - */ - public static function instance($prototype = null) - { - if ($prototype !== null) { - HTMLPurifier_ConfigSchema::$singleton = $prototype; - } elseif (HTMLPurifier_ConfigSchema::$singleton === null || $prototype === true) { - HTMLPurifier_ConfigSchema::$singleton = HTMLPurifier_ConfigSchema::makeFromSerial(); - } - return HTMLPurifier_ConfigSchema::$singleton; - } - - /** - * Defines a directive for configuration - * @warning Will fail of directive's namespace is defined. - * @warning This method's signature is slightly different from the legacy - * define() static method! Beware! - * @param string $key Name of directive - * @param mixed $default Default value of directive - * @param string $type Allowed type of the directive. See - * HTMLPurifier_DirectiveDef::$type for allowed values - * @param bool $allow_null Whether or not to allow null values - */ - public function add($key, $default, $type, $allow_null) - { - $obj = new stdClass(); - $obj->type = is_int($type) ? $type : HTMLPurifier_VarParser::$types[$type]; - if ($allow_null) { - $obj->allow_null = true; - } - $this->info[$key] = $obj; - $this->defaults[$key] = $default; - $this->defaultPlist->set($key, $default); - } - - /** - * Defines a directive value alias. - * - * Directive value aliases are convenient for developers because it lets - * them set a directive to several values and get the same result. - * @param string $key Name of Directive - * @param array $aliases Hash of aliased values to the real alias - */ - public function addValueAliases($key, $aliases) - { - if (!isset($this->info[$key]->aliases)) { - $this->info[$key]->aliases = array(); - } - foreach ($aliases as $alias => $real) { - $this->info[$key]->aliases[$alias] = $real; - } - } - - /** - * Defines a set of allowed values for a directive. - * @warning This is slightly different from the corresponding static - * method definition. - * @param string $key Name of directive - * @param array $allowed Lookup array of allowed values - */ - public function addAllowedValues($key, $allowed) - { - $this->info[$key]->allowed = $allowed; - } - - /** - * Defines a directive alias for backwards compatibility - * @param string $key Directive that will be aliased - * @param string $new_key Directive that the alias will be to - */ - public function addAlias($key, $new_key) - { - $obj = new stdClass; - $obj->key = $new_key; - $obj->isAlias = true; - $this->info[$key] = $obj; - } - - /** - * Replaces any stdClass that only has the type property with type integer. - */ - public function postProcess() - { - foreach ($this->info as $key => $v) { - if (count((array) $v) == 1) { - $this->info[$key] = $v->type; - } elseif (count((array) $v) == 2 && isset($v->allow_null)) { - $this->info[$key] = -$v->type; - } - } - } -} - -// vim: et sw=4 sts=4 + array( + * 'Directive' => new stdClass(), + * ) + * ) + * + * The stdClass may have the following properties: + * + * - If isAlias isn't set: + * - type: Integer type of directive, see HTMLPurifier_VarParser for definitions + * - allow_null: If set, this directive allows null values + * - aliases: If set, an associative array of value aliases to real values + * - allowed: If set, a lookup array of allowed (string) values + * - If isAlias is set: + * - namespace: Namespace this directive aliases to + * - name: Directive name this directive aliases to + * + * In certain degenerate cases, stdClass will actually be an integer. In + * that case, the value is equivalent to an stdClass with the type + * property set to the integer. If the integer is negative, type is + * equal to the absolute value of integer, and allow_null is true. + * + * This class is friendly with HTMLPurifier_Config. If you need introspection + * about the schema, you're better of using the ConfigSchema_Interchange, + * which uses more memory but has much richer information. + * @type array + */ + public $info = array(); + + /** + * Application-wide singleton + * @type HTMLPurifier_ConfigSchema + */ + protected static $singleton; + + public function __construct() + { + $this->defaultPlist = new HTMLPurifier_PropertyList(); + } + + /** + * Unserializes the default ConfigSchema. + * @return HTMLPurifier_ConfigSchema + */ + public static function makeFromSerial() + { + $contents = file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/ConfigSchema/schema.ser'); + $r = unserialize($contents); + if (!$r) { + $hash = sha1($contents); + trigger_error("Unserialization of configuration schema failed, sha1 of file was $hash", E_USER_ERROR); + } + return $r; + } + + /** + * Retrieves an instance of the application-wide configuration definition. + * @param HTMLPurifier_ConfigSchema $prototype + * @return HTMLPurifier_ConfigSchema + */ + public static function instance($prototype = null) + { + if ($prototype !== null) { + HTMLPurifier_ConfigSchema::$singleton = $prototype; + } elseif (HTMLPurifier_ConfigSchema::$singleton === null || $prototype === true) { + HTMLPurifier_ConfigSchema::$singleton = HTMLPurifier_ConfigSchema::makeFromSerial(); + } + return HTMLPurifier_ConfigSchema::$singleton; + } + + /** + * Defines a directive for configuration + * @warning Will fail of directive's namespace is defined. + * @warning This method's signature is slightly different from the legacy + * define() static method! Beware! + * @param string $key Name of directive + * @param mixed $default Default value of directive + * @param string $type Allowed type of the directive. See + * HTMLPurifier_VarParser::$types for allowed values + * @param bool $allow_null Whether or not to allow null values + */ + public function add($key, $default, $type, $allow_null) + { + $obj = new stdClass(); + $obj->type = is_int($type) ? $type : HTMLPurifier_VarParser::$types[$type]; + if ($allow_null) { + $obj->allow_null = true; + } + $this->info[$key] = $obj; + $this->defaults[$key] = $default; + $this->defaultPlist->set($key, $default); + } + + /** + * Defines a directive value alias. + * + * Directive value aliases are convenient for developers because it lets + * them set a directive to several values and get the same result. + * @param string $key Name of Directive + * @param array $aliases Hash of aliased values to the real alias + */ + public function addValueAliases($key, $aliases) + { + if (!isset($this->info[$key]->aliases)) { + $this->info[$key]->aliases = array(); + } + foreach ($aliases as $alias => $real) { + $this->info[$key]->aliases[$alias] = $real; + } + } + + /** + * Defines a set of allowed values for a directive. + * @warning This is slightly different from the corresponding static + * method definition. + * @param string $key Name of directive + * @param array $allowed Lookup array of allowed values + */ + public function addAllowedValues($key, $allowed) + { + $this->info[$key]->allowed = $allowed; + } + + /** + * Defines a directive alias for backwards compatibility + * @param string $key Directive that will be aliased + * @param string $new_key Directive that the alias will be to + */ + public function addAlias($key, $new_key) + { + $obj = new stdClass; + $obj->key = $new_key; + $obj->isAlias = true; + $this->info[$key] = $obj; + } + + /** + * Replaces any stdClass that only has the type property with type integer. + */ + public function postProcess() + { + foreach ($this->info as $key => $v) { + if (count((array) $v) == 1) { + $this->info[$key] = $v->type; + } elseif (count((array) $v) == 2 && isset($v->allow_null)) { + $this->info[$key] = -$v->type; + } + } + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php b/lib/purifier/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php index 1174575ea9..d5906cd46d 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php +++ b/lib/purifier/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php @@ -1,48 +1,48 @@ -directives as $d) { - $schema->add( - $d->id->key, - $d->default, - $d->type, - $d->typeAllowsNull - ); - if ($d->allowed !== null) { - $schema->addAllowedValues( - $d->id->key, - $d->allowed - ); - } - foreach ($d->aliases as $alias) { - $schema->addAlias( - $alias->key, - $d->id->key - ); - } - if ($d->valueAliases !== null) { - $schema->addValueAliases( - $d->id->key, - $d->valueAliases - ); - } - } - $schema->postProcess(); - return $schema; - } -} - -// vim: et sw=4 sts=4 +directives as $d) { + $schema->add( + $d->id->key, + $d->default, + $d->type, + $d->typeAllowsNull + ); + if ($d->allowed !== null) { + $schema->addAllowedValues( + $d->id->key, + $d->allowed + ); + } + foreach ($d->aliases as $alias) { + $schema->addAlias( + $alias->key, + $d->id->key + ); + } + if ($d->valueAliases !== null) { + $schema->addValueAliases( + $d->id->key, + $d->valueAliases + ); + } + } + $schema->postProcess(); + return $schema; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/Builder/Xml.php b/lib/purifier/HTMLPurifier/ConfigSchema/Builder/Xml.php index 0d00bf1d15..5fa56f7ddb 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/Builder/Xml.php +++ b/lib/purifier/HTMLPurifier/ConfigSchema/Builder/Xml.php @@ -1,144 +1,144 @@ -startElement('div'); - - $purifier = HTMLPurifier::getInstance(); - $html = $purifier->purify($html); - $this->writeAttribute('xmlns', 'http://www.w3.org/1999/xhtml'); - $this->writeRaw($html); - - $this->endElement(); // div - } - - /** - * @param mixed $var - * @return string - */ - protected function export($var) - { - if ($var === array()) { - return 'array()'; - } - return var_export($var, true); - } - - /** - * @param HTMLPurifier_ConfigSchema_Interchange $interchange - */ - public function build($interchange) - { - // global access, only use as last resort - $this->interchange = $interchange; - - $this->setIndent(true); - $this->startDocument('1.0', 'UTF-8'); - $this->startElement('configdoc'); - $this->writeElement('title', $interchange->name); - - foreach ($interchange->directives as $directive) { - $this->buildDirective($directive); - } - - if ($this->namespace) { - $this->endElement(); - } // namespace - - $this->endElement(); // configdoc - $this->flush(); - } - - /** - * @param HTMLPurifier_ConfigSchema_Interchange_Directive $directive - */ - public function buildDirective($directive) - { - // Kludge, although I suppose having a notion of a "root namespace" - // certainly makes things look nicer when documentation is built. - // Depends on things being sorted. - if (!$this->namespace || $this->namespace !== $directive->id->getRootNamespace()) { - if ($this->namespace) { - $this->endElement(); - } // namespace - $this->namespace = $directive->id->getRootNamespace(); - $this->startElement('namespace'); - $this->writeAttribute('id', $this->namespace); - $this->writeElement('name', $this->namespace); - } - - $this->startElement('directive'); - $this->writeAttribute('id', $directive->id->toString()); - - $this->writeElement('name', $directive->id->getDirective()); - - $this->startElement('aliases'); - foreach ($directive->aliases as $alias) { - $this->writeElement('alias', $alias->toString()); - } - $this->endElement(); // aliases - - $this->startElement('constraints'); - if ($directive->version) { - $this->writeElement('version', $directive->version); - } - $this->startElement('type'); - if ($directive->typeAllowsNull) { - $this->writeAttribute('allow-null', 'yes'); - } - $this->text($directive->type); - $this->endElement(); // type - if ($directive->allowed) { - $this->startElement('allowed'); - foreach ($directive->allowed as $value => $x) { - $this->writeElement('value', $value); - } - $this->endElement(); // allowed - } - $this->writeElement('default', $this->export($directive->default)); - $this->writeAttribute('xml:space', 'preserve'); - if ($directive->external) { - $this->startElement('external'); - foreach ($directive->external as $project) { - $this->writeElement('project', $project); - } - $this->endElement(); - } - $this->endElement(); // constraints - - if ($directive->deprecatedVersion) { - $this->startElement('deprecated'); - $this->writeElement('version', $directive->deprecatedVersion); - $this->writeElement('use', $directive->deprecatedUse->toString()); - $this->endElement(); // deprecated - } - - $this->startElement('description'); - $this->writeHTMLDiv($directive->description); - $this->endElement(); // description - - $this->endElement(); // directive - } -} - -// vim: et sw=4 sts=4 +startElement('div'); + + $purifier = HTMLPurifier::getInstance(); + $html = $purifier->purify($html); + $this->writeAttribute('xmlns', 'http://www.w3.org/1999/xhtml'); + $this->writeRaw($html); + + $this->endElement(); // div + } + + /** + * @param mixed $var + * @return string + */ + protected function export($var) + { + if ($var === array()) { + return 'array()'; + } + return var_export($var, true); + } + + /** + * @param HTMLPurifier_ConfigSchema_Interchange $interchange + */ + public function build($interchange) + { + // global access, only use as last resort + $this->interchange = $interchange; + + $this->setIndent(true); + $this->startDocument('1.0', 'UTF-8'); + $this->startElement('configdoc'); + $this->writeElement('title', $interchange->name); + + foreach ($interchange->directives as $directive) { + $this->buildDirective($directive); + } + + if ($this->namespace) { + $this->endElement(); + } // namespace + + $this->endElement(); // configdoc + $this->flush(); + } + + /** + * @param HTMLPurifier_ConfigSchema_Interchange_Directive $directive + */ + public function buildDirective($directive) + { + // Kludge, although I suppose having a notion of a "root namespace" + // certainly makes things look nicer when documentation is built. + // Depends on things being sorted. + if (!$this->namespace || $this->namespace !== $directive->id->getRootNamespace()) { + if ($this->namespace) { + $this->endElement(); + } // namespace + $this->namespace = $directive->id->getRootNamespace(); + $this->startElement('namespace'); + $this->writeAttribute('id', $this->namespace); + $this->writeElement('name', $this->namespace); + } + + $this->startElement('directive'); + $this->writeAttribute('id', $directive->id->toString()); + + $this->writeElement('name', $directive->id->getDirective()); + + $this->startElement('aliases'); + foreach ($directive->aliases as $alias) { + $this->writeElement('alias', $alias->toString()); + } + $this->endElement(); // aliases + + $this->startElement('constraints'); + if ($directive->version) { + $this->writeElement('version', $directive->version); + } + $this->startElement('type'); + if ($directive->typeAllowsNull) { + $this->writeAttribute('allow-null', 'yes'); + } + $this->text($directive->type); + $this->endElement(); // type + if ($directive->allowed) { + $this->startElement('allowed'); + foreach ($directive->allowed as $value => $x) { + $this->writeElement('value', $value); + } + $this->endElement(); // allowed + } + $this->writeElement('default', $this->export($directive->default)); + $this->writeAttribute('xml:space', 'preserve'); + if ($directive->external) { + $this->startElement('external'); + foreach ($directive->external as $project) { + $this->writeElement('project', $project); + } + $this->endElement(); + } + $this->endElement(); // constraints + + if ($directive->deprecatedVersion) { + $this->startElement('deprecated'); + $this->writeElement('version', $directive->deprecatedVersion); + $this->writeElement('use', $directive->deprecatedUse->toString()); + $this->endElement(); // deprecated + } + + $this->startElement('description'); + $this->writeHTMLDiv($directive->description); + $this->endElement(); // description + + $this->endElement(); // directive + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/Exception.php b/lib/purifier/HTMLPurifier/ConfigSchema/Exception.php index 1abdcfc065..2671516c58 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/Exception.php +++ b/lib/purifier/HTMLPurifier/ConfigSchema/Exception.php @@ -1,11 +1,11 @@ - array(directive info) - * @type HTMLPurifier_ConfigSchema_Interchange_Directive[] - */ - public $directives = array(); - - /** - * Adds a directive array to $directives - * @param HTMLPurifier_ConfigSchema_Interchange_Directive $directive - * @throws HTMLPurifier_ConfigSchema_Exception - */ - public function addDirective($directive) - { - if (isset($this->directives[$i = $directive->id->toString()])) { - throw new HTMLPurifier_ConfigSchema_Exception("Cannot redefine directive '$i'"); - } - $this->directives[$i] = $directive; - } - - /** - * Convenience function to perform standard validation. Throws exception - * on failed validation. - */ - public function validate() - { - $validator = new HTMLPurifier_ConfigSchema_Validator(); - return $validator->validate($this); - } -} - -// vim: et sw=4 sts=4 + array(directive info) + * @type HTMLPurifier_ConfigSchema_Interchange_Directive[] + */ + public $directives = array(); + + /** + * Adds a directive array to $directives + * @param HTMLPurifier_ConfigSchema_Interchange_Directive $directive + * @throws HTMLPurifier_ConfigSchema_Exception + */ + public function addDirective($directive) + { + if (isset($this->directives[$i = $directive->id->toString()])) { + throw new HTMLPurifier_ConfigSchema_Exception("Cannot redefine directive '$i'"); + } + $this->directives[$i] = $directive; + } + + /** + * Convenience function to perform standard validation. Throws exception + * on failed validation. + */ + public function validate() + { + $validator = new HTMLPurifier_ConfigSchema_Validator(); + return $validator->validate($this); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/Interchange/Directive.php b/lib/purifier/HTMLPurifier/ConfigSchema/Interchange/Directive.php index 4c39c5c688..127a39a673 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/Interchange/Directive.php +++ b/lib/purifier/HTMLPurifier/ConfigSchema/Interchange/Directive.php @@ -1,89 +1,89 @@ - true). - * Null if all values are allowed. - * @type array - */ - public $allowed; - - /** - * List of aliases for the directive. - * e.g. array(new HTMLPurifier_ConfigSchema_Interchange_Id('Ns', 'Dir'))). - * @type HTMLPurifier_ConfigSchema_Interchange_Id[] - */ - public $aliases = array(); - - /** - * Hash of value aliases, e.g. array('alt' => 'real'). Null if value - * aliasing is disabled (necessary for non-scalar types). - * @type array - */ - public $valueAliases; - - /** - * Version of HTML Purifier the directive was introduced, e.g. '1.3.1'. - * Null if the directive has always existed. - * @type string - */ - public $version; - - /** - * ID of directive that supercedes this old directive. - * Null if not deprecated. - * @type HTMLPurifier_ConfigSchema_Interchange_Id - */ - public $deprecatedUse; - - /** - * Version of HTML Purifier this directive was deprecated. Null if not - * deprecated. - * @type string - */ - public $deprecatedVersion; - - /** - * List of external projects this directive depends on, e.g. array('CSSTidy'). - * @type array - */ - public $external = array(); -} - -// vim: et sw=4 sts=4 + true). + * Null if all values are allowed. + * @type array + */ + public $allowed; + + /** + * List of aliases for the directive. + * e.g. array(new HTMLPurifier_ConfigSchema_Interchange_Id('Ns', 'Dir'))). + * @type HTMLPurifier_ConfigSchema_Interchange_Id[] + */ + public $aliases = array(); + + /** + * Hash of value aliases, e.g. array('alt' => 'real'). Null if value + * aliasing is disabled (necessary for non-scalar types). + * @type array + */ + public $valueAliases; + + /** + * Version of HTML Purifier the directive was introduced, e.g. '1.3.1'. + * Null if the directive has always existed. + * @type string + */ + public $version; + + /** + * ID of directive that supercedes this old directive. + * Null if not deprecated. + * @type HTMLPurifier_ConfigSchema_Interchange_Id + */ + public $deprecatedUse; + + /** + * Version of HTML Purifier this directive was deprecated. Null if not + * deprecated. + * @type string + */ + public $deprecatedVersion; + + /** + * List of external projects this directive depends on, e.g. array('CSSTidy'). + * @type array + */ + public $external = array(); +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/Interchange/Id.php b/lib/purifier/HTMLPurifier/ConfigSchema/Interchange/Id.php index 3ee8171141..126f09d957 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/Interchange/Id.php +++ b/lib/purifier/HTMLPurifier/ConfigSchema/Interchange/Id.php @@ -1,58 +1,58 @@ -key = $key; - } - - /** - * @return string - * @warning This is NOT magic, to ensure that people don't abuse SPL and - * cause problems for PHP 5.0 support. - */ - public function toString() - { - return $this->key; - } - - /** - * @return string - */ - public function getRootNamespace() - { - return substr($this->key, 0, strpos($this->key, ".")); - } - - /** - * @return string - */ - public function getDirective() - { - return substr($this->key, strpos($this->key, ".") + 1); - } - - /** - * @param string $id - * @return HTMLPurifier_ConfigSchema_Interchange_Id - */ - public static function make($id) - { - return new HTMLPurifier_ConfigSchema_Interchange_Id($id); - } -} - -// vim: et sw=4 sts=4 +key = $key; + } + + /** + * @return string + * @warning This is NOT magic, to ensure that people don't abuse SPL and + * cause problems for PHP 5.0 support. + */ + public function toString() + { + return $this->key; + } + + /** + * @return string + */ + public function getRootNamespace() + { + return substr($this->key, 0, strpos($this->key, ".")); + } + + /** + * @return string + */ + public function getDirective() + { + return substr($this->key, strpos($this->key, ".") + 1); + } + + /** + * @param string $id + * @return HTMLPurifier_ConfigSchema_Interchange_Id + */ + public static function make($id) + { + return new HTMLPurifier_ConfigSchema_Interchange_Id($id); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/InterchangeBuilder.php b/lib/purifier/HTMLPurifier/ConfigSchema/InterchangeBuilder.php index fe9b3268f4..655e6dd1b9 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/InterchangeBuilder.php +++ b/lib/purifier/HTMLPurifier/ConfigSchema/InterchangeBuilder.php @@ -1,226 +1,226 @@ -varParser = $varParser ? $varParser : new HTMLPurifier_VarParser_Native(); - } - - /** - * @param string $dir - * @return HTMLPurifier_ConfigSchema_Interchange - */ - public static function buildFromDirectory($dir = null) - { - $builder = new HTMLPurifier_ConfigSchema_InterchangeBuilder(); - $interchange = new HTMLPurifier_ConfigSchema_Interchange(); - return $builder->buildDir($interchange, $dir); - } - - /** - * @param HTMLPurifier_ConfigSchema_Interchange $interchange - * @param string $dir - * @return HTMLPurifier_ConfigSchema_Interchange - */ - public function buildDir($interchange, $dir = null) - { - if (!$dir) { - $dir = HTMLPURIFIER_PREFIX . '/HTMLPurifier/ConfigSchema/schema'; - } - if (file_exists($dir . '/info.ini')) { - $info = parse_ini_file($dir . '/info.ini'); - $interchange->name = $info['name']; - } - - $files = array(); - $dh = opendir($dir); - while (false !== ($file = readdir($dh))) { - if (!$file || $file[0] == '.' || strrchr($file, '.') !== '.txt') { - continue; - } - $files[] = $file; - } - closedir($dh); - - sort($files); - foreach ($files as $file) { - $this->buildFile($interchange, $dir . '/' . $file); - } - return $interchange; - } - - /** - * @param HTMLPurifier_ConfigSchema_Interchange $interchange - * @param string $file - */ - public function buildFile($interchange, $file) - { - $parser = new HTMLPurifier_StringHashParser(); - $this->build( - $interchange, - new HTMLPurifier_StringHash($parser->parseFile($file)) - ); - } - - /** - * Builds an interchange object based on a hash. - * @param HTMLPurifier_ConfigSchema_Interchange $interchange HTMLPurifier_ConfigSchema_Interchange object to build - * @param HTMLPurifier_StringHash $hash source data - * @throws HTMLPurifier_ConfigSchema_Exception - */ - public function build($interchange, $hash) - { - if (!$hash instanceof HTMLPurifier_StringHash) { - $hash = new HTMLPurifier_StringHash($hash); - } - if (!isset($hash['ID'])) { - throw new HTMLPurifier_ConfigSchema_Exception('Hash does not have any ID'); - } - if (strpos($hash['ID'], '.') === false) { - if (count($hash) == 2 && isset($hash['DESCRIPTION'])) { - $hash->offsetGet('DESCRIPTION'); // prevent complaining - } else { - throw new HTMLPurifier_ConfigSchema_Exception('All directives must have a namespace'); - } - } else { - $this->buildDirective($interchange, $hash); - } - $this->_findUnused($hash); - } - - /** - * @param HTMLPurifier_ConfigSchema_Interchange $interchange - * @param HTMLPurifier_StringHash $hash - * @throws HTMLPurifier_ConfigSchema_Exception - */ - public function buildDirective($interchange, $hash) - { - $directive = new HTMLPurifier_ConfigSchema_Interchange_Directive(); - - // These are required elements: - $directive->id = $this->id($hash->offsetGet('ID')); - $id = $directive->id->toString(); // convenience - - if (isset($hash['TYPE'])) { - $type = explode('/', $hash->offsetGet('TYPE')); - if (isset($type[1])) { - $directive->typeAllowsNull = true; - } - $directive->type = $type[0]; - } else { - throw new HTMLPurifier_ConfigSchema_Exception("TYPE in directive hash '$id' not defined"); - } - - if (isset($hash['DEFAULT'])) { - try { - $directive->default = $this->varParser->parse( - $hash->offsetGet('DEFAULT'), - $directive->type, - $directive->typeAllowsNull - ); - } catch (HTMLPurifier_VarParserException $e) { - throw new HTMLPurifier_ConfigSchema_Exception($e->getMessage() . " in DEFAULT in directive hash '$id'"); - } - } - - if (isset($hash['DESCRIPTION'])) { - $directive->description = $hash->offsetGet('DESCRIPTION'); - } - - if (isset($hash['ALLOWED'])) { - $directive->allowed = $this->lookup($this->evalArray($hash->offsetGet('ALLOWED'))); - } - - if (isset($hash['VALUE-ALIASES'])) { - $directive->valueAliases = $this->evalArray($hash->offsetGet('VALUE-ALIASES')); - } - - if (isset($hash['ALIASES'])) { - $raw_aliases = trim($hash->offsetGet('ALIASES')); - $aliases = preg_split('/\s*,\s*/', $raw_aliases); - foreach ($aliases as $alias) { - $directive->aliases[] = $this->id($alias); - } - } - - if (isset($hash['VERSION'])) { - $directive->version = $hash->offsetGet('VERSION'); - } - - if (isset($hash['DEPRECATED-USE'])) { - $directive->deprecatedUse = $this->id($hash->offsetGet('DEPRECATED-USE')); - } - - if (isset($hash['DEPRECATED-VERSION'])) { - $directive->deprecatedVersion = $hash->offsetGet('DEPRECATED-VERSION'); - } - - if (isset($hash['EXTERNAL'])) { - $directive->external = preg_split('/\s*,\s*/', trim($hash->offsetGet('EXTERNAL'))); - } - - $interchange->addDirective($directive); - } - - /** - * Evaluates an array PHP code string without array() wrapper - * @param string $contents - */ - protected function evalArray($contents) - { - return eval('return array(' . $contents . ');'); - } - - /** - * Converts an array list into a lookup array. - * @param array $array - * @return array - */ - protected function lookup($array) - { - $ret = array(); - foreach ($array as $val) { - $ret[$val] = true; - } - return $ret; - } - - /** - * Convenience function that creates an HTMLPurifier_ConfigSchema_Interchange_Id - * object based on a string Id. - * @param string $id - * @return HTMLPurifier_ConfigSchema_Interchange_Id - */ - protected function id($id) - { - return HTMLPurifier_ConfigSchema_Interchange_Id::make($id); - } - - /** - * Triggers errors for any unused keys passed in the hash; such keys - * may indicate typos, missing values, etc. - * @param HTMLPurifier_StringHash $hash Hash to check. - */ - protected function _findUnused($hash) - { - $accessed = $hash->getAccessed(); - foreach ($hash as $k => $v) { - if (!isset($accessed[$k])) { - trigger_error("String hash key '$k' not used by builder", E_USER_NOTICE); - } - } - } -} - -// vim: et sw=4 sts=4 +varParser = $varParser ? $varParser : new HTMLPurifier_VarParser_Native(); + } + + /** + * @param string $dir + * @return HTMLPurifier_ConfigSchema_Interchange + */ + public static function buildFromDirectory($dir = null) + { + $builder = new HTMLPurifier_ConfigSchema_InterchangeBuilder(); + $interchange = new HTMLPurifier_ConfigSchema_Interchange(); + return $builder->buildDir($interchange, $dir); + } + + /** + * @param HTMLPurifier_ConfigSchema_Interchange $interchange + * @param string $dir + * @return HTMLPurifier_ConfigSchema_Interchange + */ + public function buildDir($interchange, $dir = null) + { + if (!$dir) { + $dir = HTMLPURIFIER_PREFIX . '/HTMLPurifier/ConfigSchema/schema'; + } + if (file_exists($dir . '/info.ini')) { + $info = parse_ini_file($dir . '/info.ini'); + $interchange->name = $info['name']; + } + + $files = array(); + $dh = opendir($dir); + while (false !== ($file = readdir($dh))) { + if (!$file || $file[0] == '.' || strrchr($file, '.') !== '.txt') { + continue; + } + $files[] = $file; + } + closedir($dh); + + sort($files); + foreach ($files as $file) { + $this->buildFile($interchange, $dir . '/' . $file); + } + return $interchange; + } + + /** + * @param HTMLPurifier_ConfigSchema_Interchange $interchange + * @param string $file + */ + public function buildFile($interchange, $file) + { + $parser = new HTMLPurifier_StringHashParser(); + $this->build( + $interchange, + new HTMLPurifier_StringHash($parser->parseFile($file)) + ); + } + + /** + * Builds an interchange object based on a hash. + * @param HTMLPurifier_ConfigSchema_Interchange $interchange HTMLPurifier_ConfigSchema_Interchange object to build + * @param HTMLPurifier_StringHash $hash source data + * @throws HTMLPurifier_ConfigSchema_Exception + */ + public function build($interchange, $hash) + { + if (!$hash instanceof HTMLPurifier_StringHash) { + $hash = new HTMLPurifier_StringHash($hash); + } + if (!isset($hash['ID'])) { + throw new HTMLPurifier_ConfigSchema_Exception('Hash does not have any ID'); + } + if (strpos($hash['ID'], '.') === false) { + if (count($hash) == 2 && isset($hash['DESCRIPTION'])) { + $hash->offsetGet('DESCRIPTION'); // prevent complaining + } else { + throw new HTMLPurifier_ConfigSchema_Exception('All directives must have a namespace'); + } + } else { + $this->buildDirective($interchange, $hash); + } + $this->_findUnused($hash); + } + + /** + * @param HTMLPurifier_ConfigSchema_Interchange $interchange + * @param HTMLPurifier_StringHash $hash + * @throws HTMLPurifier_ConfigSchema_Exception + */ + public function buildDirective($interchange, $hash) + { + $directive = new HTMLPurifier_ConfigSchema_Interchange_Directive(); + + // These are required elements: + $directive->id = $this->id($hash->offsetGet('ID')); + $id = $directive->id->toString(); // convenience + + if (isset($hash['TYPE'])) { + $type = explode('/', $hash->offsetGet('TYPE')); + if (isset($type[1])) { + $directive->typeAllowsNull = true; + } + $directive->type = $type[0]; + } else { + throw new HTMLPurifier_ConfigSchema_Exception("TYPE in directive hash '$id' not defined"); + } + + if (isset($hash['DEFAULT'])) { + try { + $directive->default = $this->varParser->parse( + $hash->offsetGet('DEFAULT'), + $directive->type, + $directive->typeAllowsNull + ); + } catch (HTMLPurifier_VarParserException $e) { + throw new HTMLPurifier_ConfigSchema_Exception($e->getMessage() . " in DEFAULT in directive hash '$id'"); + } + } + + if (isset($hash['DESCRIPTION'])) { + $directive->description = $hash->offsetGet('DESCRIPTION'); + } + + if (isset($hash['ALLOWED'])) { + $directive->allowed = $this->lookup($this->evalArray($hash->offsetGet('ALLOWED'))); + } + + if (isset($hash['VALUE-ALIASES'])) { + $directive->valueAliases = $this->evalArray($hash->offsetGet('VALUE-ALIASES')); + } + + if (isset($hash['ALIASES'])) { + $raw_aliases = trim($hash->offsetGet('ALIASES')); + $aliases = preg_split('/\s*,\s*/', $raw_aliases); + foreach ($aliases as $alias) { + $directive->aliases[] = $this->id($alias); + } + } + + if (isset($hash['VERSION'])) { + $directive->version = $hash->offsetGet('VERSION'); + } + + if (isset($hash['DEPRECATED-USE'])) { + $directive->deprecatedUse = $this->id($hash->offsetGet('DEPRECATED-USE')); + } + + if (isset($hash['DEPRECATED-VERSION'])) { + $directive->deprecatedVersion = $hash->offsetGet('DEPRECATED-VERSION'); + } + + if (isset($hash['EXTERNAL'])) { + $directive->external = preg_split('/\s*,\s*/', trim($hash->offsetGet('EXTERNAL'))); + } + + $interchange->addDirective($directive); + } + + /** + * Evaluates an array PHP code string without array() wrapper + * @param string $contents + */ + protected function evalArray($contents) + { + return eval('return array(' . $contents . ');'); + } + + /** + * Converts an array list into a lookup array. + * @param array $array + * @return array + */ + protected function lookup($array) + { + $ret = array(); + foreach ($array as $val) { + $ret[$val] = true; + } + return $ret; + } + + /** + * Convenience function that creates an HTMLPurifier_ConfigSchema_Interchange_Id + * object based on a string Id. + * @param string $id + * @return HTMLPurifier_ConfigSchema_Interchange_Id + */ + protected function id($id) + { + return HTMLPurifier_ConfigSchema_Interchange_Id::make($id); + } + + /** + * Triggers errors for any unused keys passed in the hash; such keys + * may indicate typos, missing values, etc. + * @param HTMLPurifier_StringHash $hash Hash to check. + */ + protected function _findUnused($hash) + { + $accessed = $hash->getAccessed(); + foreach ($hash as $k => $v) { + if (!isset($accessed[$k])) { + trigger_error("String hash key '$k' not used by builder", E_USER_NOTICE); + } + } + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/Validator.php b/lib/purifier/HTMLPurifier/ConfigSchema/Validator.php index 9f14444f37..fb31277889 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/Validator.php +++ b/lib/purifier/HTMLPurifier/ConfigSchema/Validator.php @@ -1,248 +1,248 @@ -parser = new HTMLPurifier_VarParser(); - } - - /** - * Validates a fully-formed interchange object. - * @param HTMLPurifier_ConfigSchema_Interchange $interchange - * @return bool - */ - public function validate($interchange) - { - $this->interchange = $interchange; - $this->aliases = array(); - // PHP is a bit lax with integer <=> string conversions in - // arrays, so we don't use the identical !== comparison - foreach ($interchange->directives as $i => $directive) { - $id = $directive->id->toString(); - if ($i != $id) { - $this->error(false, "Integrity violation: key '$i' does not match internal id '$id'"); - } - $this->validateDirective($directive); - } - return true; - } - - /** - * Validates a HTMLPurifier_ConfigSchema_Interchange_Id object. - * @param HTMLPurifier_ConfigSchema_Interchange_Id $id - */ - public function validateId($id) - { - $id_string = $id->toString(); - $this->context[] = "id '$id_string'"; - if (!$id instanceof HTMLPurifier_ConfigSchema_Interchange_Id) { - // handled by InterchangeBuilder - $this->error(false, 'is not an instance of HTMLPurifier_ConfigSchema_Interchange_Id'); - } - // keys are now unconstrained (we might want to narrow down to A-Za-z0-9.) - // we probably should check that it has at least one namespace - $this->with($id, 'key') - ->assertNotEmpty() - ->assertIsString(); // implicit assertIsString handled by InterchangeBuilder - array_pop($this->context); - } - - /** - * Validates a HTMLPurifier_ConfigSchema_Interchange_Directive object. - * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d - */ - public function validateDirective($d) - { - $id = $d->id->toString(); - $this->context[] = "directive '$id'"; - $this->validateId($d->id); - - $this->with($d, 'description') - ->assertNotEmpty(); - - // BEGIN - handled by InterchangeBuilder - $this->with($d, 'type') - ->assertNotEmpty(); - $this->with($d, 'typeAllowsNull') - ->assertIsBool(); - try { - // This also tests validity of $d->type - $this->parser->parse($d->default, $d->type, $d->typeAllowsNull); - } catch (HTMLPurifier_VarParserException $e) { - $this->error('default', 'had error: ' . $e->getMessage()); - } - // END - handled by InterchangeBuilder - - if (!is_null($d->allowed) || !empty($d->valueAliases)) { - // allowed and valueAliases require that we be dealing with - // strings, so check for that early. - $d_int = HTMLPurifier_VarParser::$types[$d->type]; - if (!isset(HTMLPurifier_VarParser::$stringTypes[$d_int])) { - $this->error('type', 'must be a string type when used with allowed or value aliases'); - } - } - - $this->validateDirectiveAllowed($d); - $this->validateDirectiveValueAliases($d); - $this->validateDirectiveAliases($d); - - array_pop($this->context); - } - - /** - * Extra validation if $allowed member variable of - * HTMLPurifier_ConfigSchema_Interchange_Directive is defined. - * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d - */ - public function validateDirectiveAllowed($d) - { - if (is_null($d->allowed)) { - return; - } - $this->with($d, 'allowed') - ->assertNotEmpty() - ->assertIsLookup(); // handled by InterchangeBuilder - if (is_string($d->default) && !isset($d->allowed[$d->default])) { - $this->error('default', 'must be an allowed value'); - } - $this->context[] = 'allowed'; - foreach ($d->allowed as $val => $x) { - if (!is_string($val)) { - $this->error("value $val", 'must be a string'); - } - } - array_pop($this->context); - } - - /** - * Extra validation if $valueAliases member variable of - * HTMLPurifier_ConfigSchema_Interchange_Directive is defined. - * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d - */ - public function validateDirectiveValueAliases($d) - { - if (is_null($d->valueAliases)) { - return; - } - $this->with($d, 'valueAliases') - ->assertIsArray(); // handled by InterchangeBuilder - $this->context[] = 'valueAliases'; - foreach ($d->valueAliases as $alias => $real) { - if (!is_string($alias)) { - $this->error("alias $alias", 'must be a string'); - } - if (!is_string($real)) { - $this->error("alias target $real from alias '$alias'", 'must be a string'); - } - if ($alias === $real) { - $this->error("alias '$alias'", "must not be an alias to itself"); - } - } - if (!is_null($d->allowed)) { - foreach ($d->valueAliases as $alias => $real) { - if (isset($d->allowed[$alias])) { - $this->error("alias '$alias'", 'must not be an allowed value'); - } elseif (!isset($d->allowed[$real])) { - $this->error("alias '$alias'", 'must be an alias to an allowed value'); - } - } - } - array_pop($this->context); - } - - /** - * Extra validation if $aliases member variable of - * HTMLPurifier_ConfigSchema_Interchange_Directive is defined. - * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d - */ - public function validateDirectiveAliases($d) - { - $this->with($d, 'aliases') - ->assertIsArray(); // handled by InterchangeBuilder - $this->context[] = 'aliases'; - foreach ($d->aliases as $alias) { - $this->validateId($alias); - $s = $alias->toString(); - if (isset($this->interchange->directives[$s])) { - $this->error("alias '$s'", 'collides with another directive'); - } - if (isset($this->aliases[$s])) { - $other_directive = $this->aliases[$s]; - $this->error("alias '$s'", "collides with alias for directive '$other_directive'"); - } - $this->aliases[$s] = $d->id->toString(); - } - array_pop($this->context); - } - - // protected helper functions - - /** - * Convenience function for generating HTMLPurifier_ConfigSchema_ValidatorAtom - * for validating simple member variables of objects. - * @param $obj - * @param $member - * @return HTMLPurifier_ConfigSchema_ValidatorAtom - */ - protected function with($obj, $member) - { - return new HTMLPurifier_ConfigSchema_ValidatorAtom($this->getFormattedContext(), $obj, $member); - } - - /** - * Emits an error, providing helpful context. - * @throws HTMLPurifier_ConfigSchema_Exception - */ - protected function error($target, $msg) - { - if ($target !== false) { - $prefix = ucfirst($target) . ' in ' . $this->getFormattedContext(); - } else { - $prefix = ucfirst($this->getFormattedContext()); - } - throw new HTMLPurifier_ConfigSchema_Exception(trim($prefix . ' ' . $msg)); - } - - /** - * Returns a formatted context string. - * @return string - */ - protected function getFormattedContext() - { - return implode(' in ', array_reverse($this->context)); - } -} - -// vim: et sw=4 sts=4 +parser = new HTMLPurifier_VarParser(); + } + + /** + * Validates a fully-formed interchange object. + * @param HTMLPurifier_ConfigSchema_Interchange $interchange + * @return bool + */ + public function validate($interchange) + { + $this->interchange = $interchange; + $this->aliases = array(); + // PHP is a bit lax with integer <=> string conversions in + // arrays, so we don't use the identical !== comparison + foreach ($interchange->directives as $i => $directive) { + $id = $directive->id->toString(); + if ($i != $id) { + $this->error(false, "Integrity violation: key '$i' does not match internal id '$id'"); + } + $this->validateDirective($directive); + } + return true; + } + + /** + * Validates a HTMLPurifier_ConfigSchema_Interchange_Id object. + * @param HTMLPurifier_ConfigSchema_Interchange_Id $id + */ + public function validateId($id) + { + $id_string = $id->toString(); + $this->context[] = "id '$id_string'"; + if (!$id instanceof HTMLPurifier_ConfigSchema_Interchange_Id) { + // handled by InterchangeBuilder + $this->error(false, 'is not an instance of HTMLPurifier_ConfigSchema_Interchange_Id'); + } + // keys are now unconstrained (we might want to narrow down to A-Za-z0-9.) + // we probably should check that it has at least one namespace + $this->with($id, 'key') + ->assertNotEmpty() + ->assertIsString(); // implicit assertIsString handled by InterchangeBuilder + array_pop($this->context); + } + + /** + * Validates a HTMLPurifier_ConfigSchema_Interchange_Directive object. + * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d + */ + public function validateDirective($d) + { + $id = $d->id->toString(); + $this->context[] = "directive '$id'"; + $this->validateId($d->id); + + $this->with($d, 'description') + ->assertNotEmpty(); + + // BEGIN - handled by InterchangeBuilder + $this->with($d, 'type') + ->assertNotEmpty(); + $this->with($d, 'typeAllowsNull') + ->assertIsBool(); + try { + // This also tests validity of $d->type + $this->parser->parse($d->default, $d->type, $d->typeAllowsNull); + } catch (HTMLPurifier_VarParserException $e) { + $this->error('default', 'had error: ' . $e->getMessage()); + } + // END - handled by InterchangeBuilder + + if (!is_null($d->allowed) || !empty($d->valueAliases)) { + // allowed and valueAliases require that we be dealing with + // strings, so check for that early. + $d_int = HTMLPurifier_VarParser::$types[$d->type]; + if (!isset(HTMLPurifier_VarParser::$stringTypes[$d_int])) { + $this->error('type', 'must be a string type when used with allowed or value aliases'); + } + } + + $this->validateDirectiveAllowed($d); + $this->validateDirectiveValueAliases($d); + $this->validateDirectiveAliases($d); + + array_pop($this->context); + } + + /** + * Extra validation if $allowed member variable of + * HTMLPurifier_ConfigSchema_Interchange_Directive is defined. + * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d + */ + public function validateDirectiveAllowed($d) + { + if (is_null($d->allowed)) { + return; + } + $this->with($d, 'allowed') + ->assertNotEmpty() + ->assertIsLookup(); // handled by InterchangeBuilder + if (is_string($d->default) && !isset($d->allowed[$d->default])) { + $this->error('default', 'must be an allowed value'); + } + $this->context[] = 'allowed'; + foreach ($d->allowed as $val => $x) { + if (!is_string($val)) { + $this->error("value $val", 'must be a string'); + } + } + array_pop($this->context); + } + + /** + * Extra validation if $valueAliases member variable of + * HTMLPurifier_ConfigSchema_Interchange_Directive is defined. + * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d + */ + public function validateDirectiveValueAliases($d) + { + if (is_null($d->valueAliases)) { + return; + } + $this->with($d, 'valueAliases') + ->assertIsArray(); // handled by InterchangeBuilder + $this->context[] = 'valueAliases'; + foreach ($d->valueAliases as $alias => $real) { + if (!is_string($alias)) { + $this->error("alias $alias", 'must be a string'); + } + if (!is_string($real)) { + $this->error("alias target $real from alias '$alias'", 'must be a string'); + } + if ($alias === $real) { + $this->error("alias '$alias'", "must not be an alias to itself"); + } + } + if (!is_null($d->allowed)) { + foreach ($d->valueAliases as $alias => $real) { + if (isset($d->allowed[$alias])) { + $this->error("alias '$alias'", 'must not be an allowed value'); + } elseif (!isset($d->allowed[$real])) { + $this->error("alias '$alias'", 'must be an alias to an allowed value'); + } + } + } + array_pop($this->context); + } + + /** + * Extra validation if $aliases member variable of + * HTMLPurifier_ConfigSchema_Interchange_Directive is defined. + * @param HTMLPurifier_ConfigSchema_Interchange_Directive $d + */ + public function validateDirectiveAliases($d) + { + $this->with($d, 'aliases') + ->assertIsArray(); // handled by InterchangeBuilder + $this->context[] = 'aliases'; + foreach ($d->aliases as $alias) { + $this->validateId($alias); + $s = $alias->toString(); + if (isset($this->interchange->directives[$s])) { + $this->error("alias '$s'", 'collides with another directive'); + } + if (isset($this->aliases[$s])) { + $other_directive = $this->aliases[$s]; + $this->error("alias '$s'", "collides with alias for directive '$other_directive'"); + } + $this->aliases[$s] = $d->id->toString(); + } + array_pop($this->context); + } + + // protected helper functions + + /** + * Convenience function for generating HTMLPurifier_ConfigSchema_ValidatorAtom + * for validating simple member variables of objects. + * @param $obj + * @param $member + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + protected function with($obj, $member) + { + return new HTMLPurifier_ConfigSchema_ValidatorAtom($this->getFormattedContext(), $obj, $member); + } + + /** + * Emits an error, providing helpful context. + * @throws HTMLPurifier_ConfigSchema_Exception + */ + protected function error($target, $msg) + { + if ($target !== false) { + $prefix = ucfirst($target) . ' in ' . $this->getFormattedContext(); + } else { + $prefix = ucfirst($this->getFormattedContext()); + } + throw new HTMLPurifier_ConfigSchema_Exception(trim($prefix . ' ' . $msg)); + } + + /** + * Returns a formatted context string. + * @return string + */ + protected function getFormattedContext() + { + return implode(' in ', array_reverse($this->context)); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/ValidatorAtom.php b/lib/purifier/HTMLPurifier/ConfigSchema/ValidatorAtom.php index a2e0b4a1b3..c9aa3644af 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/ValidatorAtom.php +++ b/lib/purifier/HTMLPurifier/ConfigSchema/ValidatorAtom.php @@ -1,130 +1,130 @@ -context = $context; - $this->obj = $obj; - $this->member = $member; - $this->contents =& $obj->$member; - } - - /** - * @return HTMLPurifier_ConfigSchema_ValidatorAtom - */ - public function assertIsString() - { - if (!is_string($this->contents)) { - $this->error('must be a string'); - } - return $this; - } - - /** - * @return HTMLPurifier_ConfigSchema_ValidatorAtom - */ - public function assertIsBool() - { - if (!is_bool($this->contents)) { - $this->error('must be a boolean'); - } - return $this; - } - - /** - * @return HTMLPurifier_ConfigSchema_ValidatorAtom - */ - public function assertIsArray() - { - if (!is_array($this->contents)) { - $this->error('must be an array'); - } - return $this; - } - - /** - * @return HTMLPurifier_ConfigSchema_ValidatorAtom - */ - public function assertNotNull() - { - if ($this->contents === null) { - $this->error('must not be null'); - } - return $this; - } - - /** - * @return HTMLPurifier_ConfigSchema_ValidatorAtom - */ - public function assertAlnum() - { - $this->assertIsString(); - if (!ctype_alnum($this->contents)) { - $this->error('must be alphanumeric'); - } - return $this; - } - - /** - * @return HTMLPurifier_ConfigSchema_ValidatorAtom - */ - public function assertNotEmpty() - { - if (empty($this->contents)) { - $this->error('must not be empty'); - } - return $this; - } - - /** - * @return HTMLPurifier_ConfigSchema_ValidatorAtom - */ - public function assertIsLookup() - { - $this->assertIsArray(); - foreach ($this->contents as $v) { - if ($v !== true) { - $this->error('must be a lookup array'); - } - } - return $this; - } - - /** - * @param string $msg - * @throws HTMLPurifier_ConfigSchema_Exception - */ - protected function error($msg) - { - throw new HTMLPurifier_ConfigSchema_Exception(ucfirst($this->member) . ' in ' . $this->context . ' ' . $msg); - } -} - -// vim: et sw=4 sts=4 +context = $context; + $this->obj = $obj; + $this->member = $member; + $this->contents =& $obj->$member; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertIsString() + { + if (!is_string($this->contents)) { + $this->error('must be a string'); + } + return $this; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertIsBool() + { + if (!is_bool($this->contents)) { + $this->error('must be a boolean'); + } + return $this; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertIsArray() + { + if (!is_array($this->contents)) { + $this->error('must be an array'); + } + return $this; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertNotNull() + { + if ($this->contents === null) { + $this->error('must not be null'); + } + return $this; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertAlnum() + { + $this->assertIsString(); + if (!ctype_alnum($this->contents)) { + $this->error('must be alphanumeric'); + } + return $this; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertNotEmpty() + { + if (empty($this->contents)) { + $this->error('must not be empty'); + } + return $this; + } + + /** + * @return HTMLPurifier_ConfigSchema_ValidatorAtom + */ + public function assertIsLookup() + { + $this->assertIsArray(); + foreach ($this->contents as $v) { + if ($v !== true) { + $this->error('must be a lookup array'); + } + } + return $this; + } + + /** + * @param string $msg + * @throws HTMLPurifier_ConfigSchema_Exception + */ + protected function error($msg) + { + throw new HTMLPurifier_ConfigSchema_Exception(ucfirst($this->member) . ' in ' . $this->context . ' ' . $msg); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema.ser b/lib/purifier/HTMLPurifier/ConfigSchema/schema.ser index 371e948f1c76d99bacea65b4735454656858edbf..a5426c7366587b46c84cf4b485c73f012c7361b5 100644 GIT binary patch literal 24190 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 delta 268 zcmeyjhjDYw1an5yjTT{Slb85OOg_n`$ZT#^Jy};!cJdPoiOEXl(vxi@q$kG$9HItm&s4Y3!fL(HOytw3KXKks;abnV<2 IOGFhL0QW*sxc~qF diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedClasses.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedClasses.txt index 4a42382ec5..0517fed0a1 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedClasses.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedClasses.txt @@ -1,8 +1,8 @@ -Attr.AllowedClasses -TYPE: lookup/null -VERSION: 4.0.0 -DEFAULT: null ---DESCRIPTION-- -List of allowed class values in the class attribute. By default, this is null, -which means all classes are allowed. ---# vim: et sw=4 sts=4 +Attr.AllowedClasses +TYPE: lookup/null +VERSION: 4.0.0 +DEFAULT: null +--DESCRIPTION-- +List of allowed class values in the class attribute. By default, this is null, +which means all classes are allowed. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedFrameTargets.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedFrameTargets.txt index b033eb516a..249edd647b 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedFrameTargets.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedFrameTargets.txt @@ -1,12 +1,12 @@ -Attr.AllowedFrameTargets -TYPE: lookup -DEFAULT: array() ---DESCRIPTION-- -Lookup table of all allowed link frame targets. Some commonly used link -targets include _blank, _self, _parent and _top. Values should be -lowercase, as validation will be done in a case-sensitive manner despite -W3C's recommendation. XHTML 1.0 Strict does not permit the target attribute -so this directive will have no effect in that doctype. XHTML 1.1 does not -enable the Target module by default, you will have to manually enable it -(see the module documentation for more details.) ---# vim: et sw=4 sts=4 +Attr.AllowedFrameTargets +TYPE: lookup +DEFAULT: array() +--DESCRIPTION-- +Lookup table of all allowed link frame targets. Some commonly used link +targets include _blank, _self, _parent and _top. Values should be +lowercase, as validation will be done in a case-sensitive manner despite +W3C's recommendation. XHTML 1.0 Strict does not permit the target attribute +so this directive will have no effect in that doctype. XHTML 1.1 does not +enable the Target module by default, you will have to manually enable it +(see the module documentation for more details.) +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRel.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRel.txt index ed72a9d567..9a8fa6a2e2 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRel.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRel.txt @@ -1,9 +1,9 @@ -Attr.AllowedRel -TYPE: lookup -VERSION: 1.6.0 -DEFAULT: array() ---DESCRIPTION-- -List of allowed forward document relationships in the rel attribute. Common -values may be nofollow or print. By default, this is empty, meaning that no -document relationships are allowed. ---# vim: et sw=4 sts=4 +Attr.AllowedRel +TYPE: lookup +VERSION: 1.6.0 +DEFAULT: array() +--DESCRIPTION-- +List of allowed forward document relationships in the rel attribute. Common +values may be nofollow or print. By default, this is empty, meaning that no +document relationships are allowed. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRev.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRev.txt index 1ae672d019..b017883485 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRev.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRev.txt @@ -1,9 +1,9 @@ -Attr.AllowedRev -TYPE: lookup -VERSION: 1.6.0 -DEFAULT: array() ---DESCRIPTION-- -List of allowed reverse document relationships in the rev attribute. This -attribute is a bit of an edge-case; if you don't know what it is for, stay -away. ---# vim: et sw=4 sts=4 +Attr.AllowedRev +TYPE: lookup +VERSION: 1.6.0 +DEFAULT: array() +--DESCRIPTION-- +List of allowed reverse document relationships in the rev attribute. This +attribute is a bit of an edge-case; if you don't know what it is for, stay +away. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.ClassUseCDATA.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.ClassUseCDATA.txt index 119a9d2c66..e774b823b1 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.ClassUseCDATA.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.ClassUseCDATA.txt @@ -1,19 +1,19 @@ -Attr.ClassUseCDATA -TYPE: bool/null -DEFAULT: null -VERSION: 4.0.0 ---DESCRIPTION-- -If null, class will auto-detect the doctype and, if matching XHTML 1.1 or -XHTML 2.0, will use the restrictive NMTOKENS specification of class. Otherwise, -it will use a relaxed CDATA definition. If true, the relaxed CDATA definition -is forced; if false, the NMTOKENS definition is forced. To get behavior -of HTML Purifier prior to 4.0.0, set this directive to false. - -Some rational behind the auto-detection: -in previous versions of HTML Purifier, it was assumed that the form of -class was NMTOKENS, as specified by the XHTML Modularization (representing -XHTML 1.1 and XHTML 2.0). The DTDs for HTML 4.01 and XHTML 1.0, however -specify class as CDATA. HTML 5 effectively defines it as CDATA, but -with the additional constraint that each name should be unique (this is not -explicitly outlined in previous specifications). ---# vim: et sw=4 sts=4 +Attr.ClassUseCDATA +TYPE: bool/null +DEFAULT: null +VERSION: 4.0.0 +--DESCRIPTION-- +If null, class will auto-detect the doctype and, if matching XHTML 1.1 or +XHTML 2.0, will use the restrictive NMTOKENS specification of class. Otherwise, +it will use a relaxed CDATA definition. If true, the relaxed CDATA definition +is forced; if false, the NMTOKENS definition is forced. To get behavior +of HTML Purifier prior to 4.0.0, set this directive to false. + +Some rational behind the auto-detection: +in previous versions of HTML Purifier, it was assumed that the form of +class was NMTOKENS, as specified by the XHTML Modularization (representing +XHTML 1.1 and XHTML 2.0). The DTDs for HTML 4.01 and XHTML 1.0, however +specify class as CDATA. HTML 5 effectively defines it as CDATA, but +with the additional constraint that each name should be unique (this is not +explicitly outlined in previous specifications). +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultImageAlt.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultImageAlt.txt index 80b1431c3d..533165e175 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultImageAlt.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultImageAlt.txt @@ -1,11 +1,11 @@ -Attr.DefaultImageAlt -TYPE: string/null -DEFAULT: null -VERSION: 3.2.0 ---DESCRIPTION-- -This is the content of the alt tag of an image if the user had not -previously specified an alt attribute. This applies to all images without -a valid alt attribute, as opposed to %Attr.DefaultInvalidImageAlt, which -only applies to invalid images, and overrides in the case of an invalid image. -Default behavior with null is to use the basename of the src tag for the alt. ---# vim: et sw=4 sts=4 +Attr.DefaultImageAlt +TYPE: string/null +DEFAULT: null +VERSION: 3.2.0 +--DESCRIPTION-- +This is the content of the alt tag of an image if the user had not +previously specified an alt attribute. This applies to all images without +a valid alt attribute, as opposed to %Attr.DefaultInvalidImageAlt, which +only applies to invalid images, and overrides in the case of an invalid image. +Default behavior with null is to use the basename of the src tag for the alt. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImage.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImage.txt index c51000d1d7..9eb7e38469 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImage.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImage.txt @@ -1,9 +1,9 @@ -Attr.DefaultInvalidImage -TYPE: string -DEFAULT: '' ---DESCRIPTION-- -This is the default image an img tag will be pointed to if it does not have -a valid src attribute. In future versions, we may allow the image tag to -be removed completely, but due to design issues, this is not possible right -now. ---# vim: et sw=4 sts=4 +Attr.DefaultInvalidImage +TYPE: string +DEFAULT: '' +--DESCRIPTION-- +This is the default image an img tag will be pointed to if it does not have +a valid src attribute. In future versions, we may allow the image tag to +be removed completely, but due to design issues, this is not possible right +now. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImageAlt.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImageAlt.txt index c1ec4b038b..2f17bf477a 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImageAlt.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImageAlt.txt @@ -1,8 +1,8 @@ -Attr.DefaultInvalidImageAlt -TYPE: string -DEFAULT: 'Invalid image' ---DESCRIPTION-- -This is the content of the alt tag of an invalid image if the user had not -previously specified an alt attribute. It has no effect when the image is -valid but there was no alt attribute present. ---# vim: et sw=4 sts=4 +Attr.DefaultInvalidImageAlt +TYPE: string +DEFAULT: 'Invalid image' +--DESCRIPTION-- +This is the content of the alt tag of an invalid image if the user had not +previously specified an alt attribute. It has no effect when the image is +valid but there was no alt attribute present. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultTextDir.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultTextDir.txt index f57dcc40f2..52654b53ae 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultTextDir.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultTextDir.txt @@ -1,10 +1,10 @@ -Attr.DefaultTextDir -TYPE: string -DEFAULT: 'ltr' ---DESCRIPTION-- -Defines the default text direction (ltr or rtl) of the document being -parsed. This generally is the same as the value of the dir attribute in -HTML, or ltr if that is not specified. ---ALLOWED-- -'ltr', 'rtl' ---# vim: et sw=4 sts=4 +Attr.DefaultTextDir +TYPE: string +DEFAULT: 'ltr' +--DESCRIPTION-- +Defines the default text direction (ltr or rtl) of the document being +parsed. This generally is the same as the value of the dir attribute in +HTML, or ltr if that is not specified. +--ALLOWED-- +'ltr', 'rtl' +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.EnableID.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.EnableID.txt index 9b93a5575e..6440d21032 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.EnableID.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.EnableID.txt @@ -1,16 +1,16 @@ -Attr.EnableID -TYPE: bool -DEFAULT: false -VERSION: 1.2.0 ---DESCRIPTION-- -Allows the ID attribute in HTML. This is disabled by default due to the -fact that without proper configuration user input can easily break the -validation of a webpage by specifying an ID that is already on the -surrounding HTML. If you don't mind throwing caution to the wind, enable -this directive, but I strongly recommend you also consider blacklisting IDs -you use (%Attr.IDBlacklist) or prefixing all user supplied IDs -(%Attr.IDPrefix). When set to true HTML Purifier reverts to the behavior of -pre-1.2.0 versions. ---ALIASES-- -HTML.EnableAttrID ---# vim: et sw=4 sts=4 +Attr.EnableID +TYPE: bool +DEFAULT: false +VERSION: 1.2.0 +--DESCRIPTION-- +Allows the ID attribute in HTML. This is disabled by default due to the +fact that without proper configuration user input can easily break the +validation of a webpage by specifying an ID that is already on the +surrounding HTML. If you don't mind throwing caution to the wind, enable +this directive, but I strongly recommend you also consider blacklisting IDs +you use (%Attr.IDBlacklist) or prefixing all user supplied IDs +(%Attr.IDPrefix). When set to true HTML Purifier reverts to the behavior of +pre-1.2.0 versions. +--ALIASES-- +HTML.EnableAttrID +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.ForbiddenClasses.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.ForbiddenClasses.txt index fed8954cf7..f31d226f58 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.ForbiddenClasses.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.ForbiddenClasses.txt @@ -1,8 +1,8 @@ -Attr.ForbiddenClasses -TYPE: lookup -VERSION: 4.0.0 -DEFAULT: array() ---DESCRIPTION-- -List of forbidden class values in the class attribute. By default, this is -empty, which means that no classes are forbidden. See also %Attr.AllowedClasses. ---# vim: et sw=4 sts=4 +Attr.ForbiddenClasses +TYPE: lookup +VERSION: 4.0.0 +DEFAULT: array() +--DESCRIPTION-- +List of forbidden class values in the class attribute. By default, this is +empty, which means that no classes are forbidden. See also %Attr.AllowedClasses. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.ID.HTML5.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.ID.HTML5.txt index c48e62fbe3..735d4b7a10 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.ID.HTML5.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.ID.HTML5.txt @@ -1,10 +1,10 @@ -Attr.ID.HTML5 -TYPE: bool/null -DEFAULT: null -VERSION: 4.8.0 ---DESCRIPTION-- -In HTML5, restrictions on the format of the id attribute have been significantly -relaxed, such that any string is valid so long as it contains no spaces and -is at least one character. In lieu of a general HTML5 compatibility flag, -set this configuration directive to true to use the relaxed rules. ---# vim: et sw=4 sts=4 +Attr.ID.HTML5 +TYPE: bool/null +DEFAULT: null +VERSION: 4.8.0 +--DESCRIPTION-- +In HTML5, restrictions on the format of the id attribute have been significantly +relaxed, such that any string is valid so long as it contains no spaces and +is at least one character. In lieu of a general HTML5 compatibility flag, +set this configuration directive to true to use the relaxed rules. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklist.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklist.txt index 52168bb5e5..5f2b5e3d2c 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklist.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklist.txt @@ -1,5 +1,5 @@ -Attr.IDBlacklist -TYPE: list -DEFAULT: array() -DESCRIPTION: Array of IDs not allowed in the document. ---# vim: et sw=4 sts=4 +Attr.IDBlacklist +TYPE: list +DEFAULT: array() +DESCRIPTION: Array of IDs not allowed in the document. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklistRegexp.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklistRegexp.txt index 7b85043077..6f5824586e 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklistRegexp.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklistRegexp.txt @@ -1,9 +1,9 @@ -Attr.IDBlacklistRegexp -TYPE: string/null -VERSION: 1.6.0 -DEFAULT: NULL ---DESCRIPTION-- -PCRE regular expression to be matched against all IDs. If the expression is -matches, the ID is rejected. Use this with care: may cause significant -degradation. ID matching is done after all other validation. ---# vim: et sw=4 sts=4 +Attr.IDBlacklistRegexp +TYPE: string/null +VERSION: 1.6.0 +DEFAULT: NULL +--DESCRIPTION-- +PCRE regular expression to be matched against all IDs. If the expression is +matches, the ID is rejected. Use this with care: may cause significant +degradation. ID matching is done after all other validation. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefix.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefix.txt index 5781382775..cc49d43fd0 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefix.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefix.txt @@ -1,12 +1,12 @@ -Attr.IDPrefix -TYPE: string -VERSION: 1.2.0 -DEFAULT: '' ---DESCRIPTION-- -String to prefix to IDs. If you have no idea what IDs your pages may use, -you may opt to simply add a prefix to all user-submitted ID attributes so -that they are still usable, but will not conflict with core page IDs. -Example: setting the directive to 'user_' will result in a user submitted -'foo' to become 'user_foo' Be sure to set %HTML.EnableAttrID to true -before using this. ---# vim: et sw=4 sts=4 +Attr.IDPrefix +TYPE: string +VERSION: 1.2.0 +DEFAULT: '' +--DESCRIPTION-- +String to prefix to IDs. If you have no idea what IDs your pages may use, +you may opt to simply add a prefix to all user-submitted ID attributes so +that they are still usable, but will not conflict with core page IDs. +Example: setting the directive to 'user_' will result in a user submitted +'foo' to become 'user_foo' Be sure to set %HTML.EnableAttrID to true +before using this. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt index f91fcd602b..2c5924a7ad 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt @@ -1,14 +1,14 @@ -Attr.IDPrefixLocal -TYPE: string -VERSION: 1.2.0 -DEFAULT: '' ---DESCRIPTION-- -Temporary prefix for IDs used in conjunction with %Attr.IDPrefix. If you -need to allow multiple sets of user content on web page, you may need to -have a seperate prefix that changes with each iteration. This way, -seperately submitted user content displayed on the same page doesn't -clobber each other. Ideal values are unique identifiers for the content it -represents (i.e. the id of the row in the database). Be sure to add a -seperator (like an underscore) at the end. Warning: this directive will -not work unless %Attr.IDPrefix is set to a non-empty value! ---# vim: et sw=4 sts=4 +Attr.IDPrefixLocal +TYPE: string +VERSION: 1.2.0 +DEFAULT: '' +--DESCRIPTION-- +Temporary prefix for IDs used in conjunction with %Attr.IDPrefix. If you +need to allow multiple sets of user content on web page, you may need to +have a seperate prefix that changes with each iteration. This way, +seperately submitted user content displayed on the same page doesn't +clobber each other. Ideal values are unique identifiers for the content it +represents (i.e. the id of the row in the database). Be sure to add a +seperator (like an underscore) at the end. Warning: this directive will +not work unless %Attr.IDPrefix is set to a non-empty value! +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.AutoParagraph.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.AutoParagraph.txt index 2d7f94e023..d5caa1bb97 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.AutoParagraph.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.AutoParagraph.txt @@ -1,31 +1,31 @@ -AutoFormat.AutoParagraph -TYPE: bool -VERSION: 2.0.1 -DEFAULT: false ---DESCRIPTION-- - -

- This directive turns on auto-paragraphing, where double newlines are - converted in to paragraphs whenever possible. Auto-paragraphing: -

-
    -
  • Always applies to inline elements or text in the root node,
  • -
  • Applies to inline elements or text with double newlines in nodes - that allow paragraph tags,
  • -
  • Applies to double newlines in paragraph tags
  • -
-

- p tags must be allowed for this directive to take effect. - We do not use br tags for paragraphing, as that is - semantically incorrect. -

-

- To prevent auto-paragraphing as a content-producer, refrain from using - double-newlines except to specify a new paragraph or in contexts where - it has special meaning (whitespace usually has no meaning except in - tags like pre, so this should not be difficult.) To prevent - the paragraphing of inline text adjacent to block elements, wrap them - in div tags (the behavior is slightly different outside of - the root node.) -

---# vim: et sw=4 sts=4 +AutoFormat.AutoParagraph +TYPE: bool +VERSION: 2.0.1 +DEFAULT: false +--DESCRIPTION-- + +

+ This directive turns on auto-paragraphing, where double newlines are + converted in to paragraphs whenever possible. Auto-paragraphing: +

+
    +
  • Always applies to inline elements or text in the root node,
  • +
  • Applies to inline elements or text with double newlines in nodes + that allow paragraph tags,
  • +
  • Applies to double newlines in paragraph tags
  • +
+

+ p tags must be allowed for this directive to take effect. + We do not use br tags for paragraphing, as that is + semantically incorrect. +

+

+ To prevent auto-paragraphing as a content-producer, refrain from using + double-newlines except to specify a new paragraph or in contexts where + it has special meaning (whitespace usually has no meaning except in + tags like pre, so this should not be difficult.) To prevent + the paragraphing of inline text adjacent to block elements, wrap them + in div tags (the behavior is slightly different outside of + the root node.) +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.Custom.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.Custom.txt index 2eb1974fd0..2a476481af 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.Custom.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.Custom.txt @@ -1,12 +1,12 @@ -AutoFormat.Custom -TYPE: list -VERSION: 2.0.1 -DEFAULT: array() ---DESCRIPTION-- - -

- This directive can be used to add custom auto-format injectors. - Specify an array of injector names (class name minus the prefix) - or concrete implementations. Injector class must exist. -

---# vim: et sw=4 sts=4 +AutoFormat.Custom +TYPE: list +VERSION: 2.0.1 +DEFAULT: array() +--DESCRIPTION-- + +

+ This directive can be used to add custom auto-format injectors. + Specify an array of injector names (class name minus the prefix) + or concrete implementations. Injector class must exist. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.DisplayLinkURI.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.DisplayLinkURI.txt index c955de7f62..663064a344 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.DisplayLinkURI.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.DisplayLinkURI.txt @@ -1,11 +1,11 @@ -AutoFormat.DisplayLinkURI -TYPE: bool -VERSION: 3.2.0 -DEFAULT: false ---DESCRIPTION-- -

- This directive turns on the in-text display of URIs in <a> tags, and disables - those links. For example, example becomes - example (http://example.com). -

---# vim: et sw=4 sts=4 +AutoFormat.DisplayLinkURI +TYPE: bool +VERSION: 3.2.0 +DEFAULT: false +--DESCRIPTION-- +

+ This directive turns on the in-text display of URIs in <a> tags, and disables + those links. For example, example becomes + example (http://example.com). +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.Linkify.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.Linkify.txt index 328b2b2bfe..3a48ba960e 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.Linkify.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.Linkify.txt @@ -1,12 +1,12 @@ -AutoFormat.Linkify -TYPE: bool -VERSION: 2.0.1 -DEFAULT: false ---DESCRIPTION-- - -

- This directive turns on linkification, auto-linking http, ftp and - https URLs. a tags with the href attribute - must be allowed. -

---# vim: et sw=4 sts=4 +AutoFormat.Linkify +TYPE: bool +VERSION: 2.0.1 +DEFAULT: false +--DESCRIPTION-- + +

+ This directive turns on linkification, auto-linking http, ftp and + https URLs. a tags with the href attribute + must be allowed. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.DocURL.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.DocURL.txt index d0532b6ba4..db58b13464 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.DocURL.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.DocURL.txt @@ -1,12 +1,12 @@ -AutoFormat.PurifierLinkify.DocURL -TYPE: string -VERSION: 2.0.1 -DEFAULT: '#%s' -ALIASES: AutoFormatParam.PurifierLinkifyDocURL ---DESCRIPTION-- -

- Location of configuration documentation to link to, let %s substitute - into the configuration's namespace and directive names sans the percent - sign. -

---# vim: et sw=4 sts=4 +AutoFormat.PurifierLinkify.DocURL +TYPE: string +VERSION: 2.0.1 +DEFAULT: '#%s' +ALIASES: AutoFormatParam.PurifierLinkifyDocURL +--DESCRIPTION-- +

+ Location of configuration documentation to link to, let %s substitute + into the configuration's namespace and directive names sans the percent + sign. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.txt index f3ab259a1b..7996488be0 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.txt @@ -1,12 +1,12 @@ -AutoFormat.PurifierLinkify -TYPE: bool -VERSION: 2.0.1 -DEFAULT: false ---DESCRIPTION-- - -

- Internal auto-formatter that converts configuration directives in - syntax %Namespace.Directive to links. a tags - with the href attribute must be allowed. -

---# vim: et sw=4 sts=4 +AutoFormat.PurifierLinkify +TYPE: bool +VERSION: 2.0.1 +DEFAULT: false +--DESCRIPTION-- + +

+ Internal auto-formatter that converts configuration directives in + syntax %Namespace.Directive to links. a tags + with the href attribute must be allowed. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.Predicate.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.Predicate.txt index 376f771ea2..6367fe23c9 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.Predicate.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.Predicate.txt @@ -1,14 +1,14 @@ -AutoFormat.RemoveEmpty.Predicate -TYPE: hash -VERSION: 4.7.0 -DEFAULT: array('colgroup' => array(), 'th' => array(), 'td' => array(), 'iframe' => array('src')) ---DESCRIPTION-- -

- Given that an element has no contents, it will be removed by default, unless - this predicate dictates otherwise. The predicate can either be an associative - map from tag name to list of attributes that must be present for the element - to be considered preserved: thus, the default always preserves colgroup, - th and td, and also iframe if it - has a src. -

---# vim: et sw=4 sts=4 +AutoFormat.RemoveEmpty.Predicate +TYPE: hash +VERSION: 4.7.0 +DEFAULT: array('colgroup' => array(), 'th' => array(), 'td' => array(), 'iframe' => array('src')) +--DESCRIPTION-- +

+ Given that an element has no contents, it will be removed by default, unless + this predicate dictates otherwise. The predicate can either be an associative + map from tag name to list of attributes that must be present for the element + to be considered preserved: thus, the default always preserves colgroup, + th and td, and also iframe if it + has a src. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions.txt index 219d04ac4c..35c393b4e6 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions.txt @@ -1,11 +1,11 @@ -AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions -TYPE: lookup -VERSION: 4.0.0 -DEFAULT: array('td' => true, 'th' => true) ---DESCRIPTION-- -

- When %AutoFormat.RemoveEmpty and %AutoFormat.RemoveEmpty.RemoveNbsp - are enabled, this directive defines what HTML elements should not be - removede if they have only a non-breaking space in them. -

---# vim: et sw=4 sts=4 +AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions +TYPE: lookup +VERSION: 4.0.0 +DEFAULT: array('td' => true, 'th' => true) +--DESCRIPTION-- +

+ When %AutoFormat.RemoveEmpty and %AutoFormat.RemoveEmpty.RemoveNbsp + are enabled, this directive defines what HTML elements should not be + removede if they have only a non-breaking space in them. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.txt index 5f355d6622..9228dee228 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.txt @@ -1,15 +1,15 @@ -AutoFormat.RemoveEmpty.RemoveNbsp -TYPE: bool -VERSION: 4.0.0 -DEFAULT: false ---DESCRIPTION-- -

- When enabled, HTML Purifier will treat any elements that contain only - non-breaking spaces as well as regular whitespace as empty, and remove - them when %AutoForamt.RemoveEmpty is enabled. -

-

- See %AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions for a list of elements - that don't have this behavior applied to them. -

---# vim: et sw=4 sts=4 +AutoFormat.RemoveEmpty.RemoveNbsp +TYPE: bool +VERSION: 4.0.0 +DEFAULT: false +--DESCRIPTION-- +

+ When enabled, HTML Purifier will treat any elements that contain only + non-breaking spaces as well as regular whitespace as empty, and remove + them when %AutoFormat.RemoveEmpty is enabled. +

+

+ See %AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions for a list of elements + that don't have this behavior applied to them. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.txt index 6b5a7a5c9a..34657ba47b 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.txt @@ -1,46 +1,46 @@ -AutoFormat.RemoveEmpty -TYPE: bool -VERSION: 3.2.0 -DEFAULT: false ---DESCRIPTION-- -

- When enabled, HTML Purifier will attempt to remove empty elements that - contribute no semantic information to the document. The following types - of nodes will be removed: -

-
  • - Tags with no attributes and no content, and that are not empty - elements (remove <a></a> but not - <br />), and -
  • -
  • - Tags with no content, except for:
      -
    • The colgroup element, or
    • -
    • - Elements with the id or name attribute, - when those attributes are permitted on those elements. -
    • -
  • -
-

- Please be very careful when using this functionality; while it may not - seem that empty elements contain useful information, they can alter the - layout of a document given appropriate styling. This directive is most - useful when you are processing machine-generated HTML, please avoid using - it on regular user HTML. -

-

- Elements that contain only whitespace will be treated as empty. Non-breaking - spaces, however, do not count as whitespace. See - %AutoFormat.RemoveEmpty.RemoveNbsp for alternate behavior. -

-

- This algorithm is not perfect; you may still notice some empty tags, - particularly if a node had elements, but those elements were later removed - because they were not permitted in that context, or tags that, after - being auto-closed by another tag, where empty. This is for safety reasons - to prevent clever code from breaking validation. The general rule of thumb: - if a tag looked empty on the way in, it will get removed; if HTML Purifier - made it empty, it will stay. -

---# vim: et sw=4 sts=4 +AutoFormat.RemoveEmpty +TYPE: bool +VERSION: 3.2.0 +DEFAULT: false +--DESCRIPTION-- +

+ When enabled, HTML Purifier will attempt to remove empty elements that + contribute no semantic information to the document. The following types + of nodes will be removed: +

+
  • + Tags with no attributes and no content, and that are not empty + elements (remove <a></a> but not + <br />), and +
  • +
  • + Tags with no content, except for:
      +
    • The colgroup element, or
    • +
    • + Elements with the id or name attribute, + when those attributes are permitted on those elements. +
    • +
  • +
+

+ Please be very careful when using this functionality; while it may not + seem that empty elements contain useful information, they can alter the + layout of a document given appropriate styling. This directive is most + useful when you are processing machine-generated HTML, please avoid using + it on regular user HTML. +

+

+ Elements that contain only whitespace will be treated as empty. Non-breaking + spaces, however, do not count as whitespace. See + %AutoFormat.RemoveEmpty.RemoveNbsp for alternate behavior. +

+

+ This algorithm is not perfect; you may still notice some empty tags, + particularly if a node had elements, but those elements were later removed + because they were not permitted in that context, or tags that, after + being auto-closed by another tag, where empty. This is for safety reasons + to prevent clever code from breaking validation. The general rule of thumb: + if a tag looked empty on the way in, it will get removed; if HTML Purifier + made it empty, it will stay. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveSpansWithoutAttributes.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveSpansWithoutAttributes.txt index a448770e50..dde990ab26 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveSpansWithoutAttributes.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveSpansWithoutAttributes.txt @@ -1,11 +1,11 @@ -AutoFormat.RemoveSpansWithoutAttributes -TYPE: bool -VERSION: 4.0.1 -DEFAULT: false ---DESCRIPTION-- -

- This directive causes span tags without any attributes - to be removed. It will also remove spans that had all attributes - removed during processing. -

---# vim: et sw=4 sts=4 +AutoFormat.RemoveSpansWithoutAttributes +TYPE: bool +VERSION: 4.0.1 +DEFAULT: false +--DESCRIPTION-- +

+ This directive causes span tags without any attributes + to be removed. It will also remove spans that had all attributes + removed during processing. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowDuplicates.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowDuplicates.txt index acfeab3c87..4d054b1f07 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowDuplicates.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowDuplicates.txt @@ -1,11 +1,11 @@ -CSS.AllowDuplicates -TYPE: bool -DEFAULT: false -VERSION: 4.8.0 ---DESCRIPTION-- -

- By default, HTML Purifier removes duplicate CSS properties, - like color:red; color:blue. If this is set to - true, duplicate properties are allowed. -

---# vim: et sw=4 sts=4 +CSS.AllowDuplicates +TYPE: bool +DEFAULT: false +VERSION: 4.8.0 +--DESCRIPTION-- +

+ By default, HTML Purifier removes duplicate CSS properties, + like color:red; color:blue. If this is set to + true, duplicate properties are allowed. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowImportant.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowImportant.txt index 8096eb01ae..b324608f76 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowImportant.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowImportant.txt @@ -1,8 +1,8 @@ -CSS.AllowImportant -TYPE: bool -DEFAULT: false -VERSION: 3.1.0 ---DESCRIPTION-- -This parameter determines whether or not !important cascade modifiers should -be allowed in user CSS. If false, !important will stripped. ---# vim: et sw=4 sts=4 +CSS.AllowImportant +TYPE: bool +DEFAULT: false +VERSION: 3.1.0 +--DESCRIPTION-- +This parameter determines whether or not !important cascade modifiers should +be allowed in user CSS. If false, !important will stripped. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowTricky.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowTricky.txt index 9d34debc45..748be0eec8 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowTricky.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowTricky.txt @@ -1,11 +1,11 @@ -CSS.AllowTricky -TYPE: bool -DEFAULT: false -VERSION: 3.1.0 ---DESCRIPTION-- -This parameter determines whether or not to allow "tricky" CSS properties and -values. Tricky CSS properties/values can drastically modify page layout or -be used for deceptive practices but do not directly constitute a security risk. -For example, display:none; is considered a tricky property that -will only be allowed if this directive is set to true. ---# vim: et sw=4 sts=4 +CSS.AllowTricky +TYPE: bool +DEFAULT: false +VERSION: 3.1.0 +--DESCRIPTION-- +This parameter determines whether or not to allow "tricky" CSS properties and +values. Tricky CSS properties/values can drastically modify page layout or +be used for deceptive practices but do not directly constitute a security risk. +For example, display:none; is considered a tricky property that +will only be allowed if this directive is set to true. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowedFonts.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowedFonts.txt index 7c2b54763e..3fd4654065 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowedFonts.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowedFonts.txt @@ -1,12 +1,12 @@ -CSS.AllowedFonts -TYPE: lookup/null -VERSION: 4.3.0 -DEFAULT: NULL ---DESCRIPTION-- -

- Allows you to manually specify a set of allowed fonts. If - NULL, all fonts are allowed. This directive - affects generic names (serif, sans-serif, monospace, cursive, - fantasy) as well as specific font families. -

---# vim: et sw=4 sts=4 +CSS.AllowedFonts +TYPE: lookup/null +VERSION: 4.3.0 +DEFAULT: NULL +--DESCRIPTION-- +

+ Allows you to manually specify a set of allowed fonts. If + NULL, all fonts are allowed. This directive + affects generic names (serif, sans-serif, monospace, cursive, + fantasy) as well as specific font families. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowedProperties.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowedProperties.txt index f1ba513c3f..460112ebe0 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowedProperties.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowedProperties.txt @@ -1,18 +1,18 @@ -CSS.AllowedProperties -TYPE: lookup/null -VERSION: 3.1.0 -DEFAULT: NULL ---DESCRIPTION-- - -

- If HTML Purifier's style attributes set is unsatisfactory for your needs, - you can overload it with your own list of tags to allow. Note that this - method is subtractive: it does its job by taking away from HTML Purifier - usual feature set, so you cannot add an attribute that HTML Purifier never - supported in the first place. -

-

- Warning: If another directive conflicts with the - elements here, that directive will win and override. -

---# vim: et sw=4 sts=4 +CSS.AllowedProperties +TYPE: lookup/null +VERSION: 3.1.0 +DEFAULT: NULL +--DESCRIPTION-- + +

+ If HTML Purifier's style attributes set is unsatisfactory for your needs, + you can overload it with your own list of tags to allow. Note that this + method is subtractive: it does its job by taking away from HTML Purifier + usual feature set, so you cannot add an attribute that HTML Purifier never + supported in the first place. +

+

+ Warning: If another directive conflicts with the + elements here, that directive will win and override. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.DefinitionRev.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.DefinitionRev.txt index 96b410829e..5cb7dda3ba 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.DefinitionRev.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.DefinitionRev.txt @@ -1,11 +1,11 @@ -CSS.DefinitionRev -TYPE: int -VERSION: 2.0.0 -DEFAULT: 1 ---DESCRIPTION-- - -

- Revision identifier for your custom definition. See - %HTML.DefinitionRev for details. -

---# vim: et sw=4 sts=4 +CSS.DefinitionRev +TYPE: int +VERSION: 2.0.0 +DEFAULT: 1 +--DESCRIPTION-- + +

+ Revision identifier for your custom definition. See + %HTML.DefinitionRev for details. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.ForbiddenProperties.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.ForbiddenProperties.txt index 923e8e995c..f1f5c5f12b 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.ForbiddenProperties.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.ForbiddenProperties.txt @@ -1,13 +1,13 @@ -CSS.ForbiddenProperties -TYPE: lookup -VERSION: 4.2.0 -DEFAULT: array() ---DESCRIPTION-- -

- This is the logical inverse of %CSS.AllowedProperties, and it will - override that directive or any other directive. If possible, - %CSS.AllowedProperties is recommended over this directive, - because it can sometimes be difficult to tell whether or not you've - forbidden all of the CSS properties you truly would like to disallow. -

---# vim: et sw=4 sts=4 +CSS.ForbiddenProperties +TYPE: lookup +VERSION: 4.2.0 +DEFAULT: array() +--DESCRIPTION-- +

+ This is the logical inverse of %CSS.AllowedProperties, and it will + override that directive or any other directive. If possible, + %CSS.AllowedProperties is recommended over this directive, + because it can sometimes be difficult to tell whether or not you've + forbidden all of the CSS properties you truly would like to disallow. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.MaxImgLength.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.MaxImgLength.txt index 3808581e2b..7a3291470c 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.MaxImgLength.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.MaxImgLength.txt @@ -1,16 +1,16 @@ -CSS.MaxImgLength -TYPE: string/null -DEFAULT: '1200px' -VERSION: 3.1.1 ---DESCRIPTION-- -

- This parameter sets the maximum allowed length on img tags, - effectively the width and height properties. - Only absolute units of measurement (in, pt, pc, mm, cm) and pixels (px) are allowed. This is - in place to prevent imagecrash attacks, disable with null at your own risk. - This directive is similar to %HTML.MaxImgLength, and both should be - concurrently edited, although there are - subtle differences in the input format (the CSS max is a number with - a unit). -

---# vim: et sw=4 sts=4 +CSS.MaxImgLength +TYPE: string/null +DEFAULT: '1200px' +VERSION: 3.1.1 +--DESCRIPTION-- +

+ This parameter sets the maximum allowed length on img tags, + effectively the width and height properties. + Only absolute units of measurement (in, pt, pc, mm, cm) and pixels (px) are allowed. This is + in place to prevent imagecrash attacks, disable with null at your own risk. + This directive is similar to %HTML.MaxImgLength, and both should be + concurrently edited, although there are + subtle differences in the input format (the CSS max is a number with + a unit). +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.Proprietary.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.Proprietary.txt index 8a26f228dd..148eedb8be 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.Proprietary.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.Proprietary.txt @@ -1,10 +1,10 @@ -CSS.Proprietary -TYPE: bool -VERSION: 3.0.0 -DEFAULT: false ---DESCRIPTION-- - -

- Whether or not to allow safe, proprietary CSS values. -

---# vim: et sw=4 sts=4 +CSS.Proprietary +TYPE: bool +VERSION: 3.0.0 +DEFAULT: false +--DESCRIPTION-- + +

+ Whether or not to allow safe, proprietary CSS values. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.Trusted.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.Trusted.txt index 917ec42ba4..e733a61e8a 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.Trusted.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/CSS.Trusted.txt @@ -1,9 +1,9 @@ -CSS.Trusted -TYPE: bool -VERSION: 4.2.1 -DEFAULT: false ---DESCRIPTION-- -Indicates whether or not the user's CSS input is trusted or not. If the -input is trusted, a more expansive set of allowed properties. See -also %HTML.Trusted. ---# vim: et sw=4 sts=4 +CSS.Trusted +TYPE: bool +VERSION: 4.2.1 +DEFAULT: false +--DESCRIPTION-- +Indicates whether or not the user's CSS input is trusted or not. If the +input is trusted, a more expansive set of allowed properties. See +also %HTML.Trusted. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Cache.DefinitionImpl.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Cache.DefinitionImpl.txt index afc6a87a64..c486724c88 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Cache.DefinitionImpl.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Cache.DefinitionImpl.txt @@ -1,14 +1,14 @@ -Cache.DefinitionImpl -TYPE: string/null -VERSION: 2.0.0 -DEFAULT: 'Serializer' ---DESCRIPTION-- - -This directive defines which method to use when caching definitions, -the complex data-type that makes HTML Purifier tick. Set to null -to disable caching (not recommended, as you will see a definite -performance degradation). - ---ALIASES-- -Core.DefinitionCache ---# vim: et sw=4 sts=4 +Cache.DefinitionImpl +TYPE: string/null +VERSION: 2.0.0 +DEFAULT: 'Serializer' +--DESCRIPTION-- + +This directive defines which method to use when caching definitions, +the complex data-type that makes HTML Purifier tick. Set to null +to disable caching (not recommended, as you will see a definite +performance degradation). + +--ALIASES-- +Core.DefinitionCache +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPath.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPath.txt index 668f248af4..54036507d6 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPath.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPath.txt @@ -1,13 +1,13 @@ -Cache.SerializerPath -TYPE: string/null -VERSION: 2.0.0 -DEFAULT: NULL ---DESCRIPTION-- - -

- Absolute path with no trailing slash to store serialized definitions in. - Default is within the - HTML Purifier library inside DefinitionCache/Serializer. This - path must be writable by the webserver. -

---# vim: et sw=4 sts=4 +Cache.SerializerPath +TYPE: string/null +VERSION: 2.0.0 +DEFAULT: NULL +--DESCRIPTION-- + +

+ Absolute path with no trailing slash to store serialized definitions in. + Default is within the + HTML Purifier library inside DefinitionCache/Serializer. This + path must be writable by the webserver. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPermissions.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPermissions.txt index f6059e6720..2e0cc81044 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPermissions.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPermissions.txt @@ -1,16 +1,16 @@ -Cache.SerializerPermissions -TYPE: int/null -VERSION: 4.3.0 -DEFAULT: 0755 ---DESCRIPTION-- - -

- Directory permissions of the files and directories created inside - the DefinitionCache/Serializer or other custom serializer path. -

-

- In HTML Purifier 4.8.0, this also supports NULL, - which means that no chmod'ing or directory creation shall - occur. -

---# vim: et sw=4 sts=4 +Cache.SerializerPermissions +TYPE: int/null +VERSION: 4.3.0 +DEFAULT: 0755 +--DESCRIPTION-- + +

+ Directory permissions of the files and directories created inside + the DefinitionCache/Serializer or other custom serializer path. +

+

+ In HTML Purifier 4.8.0, this also supports NULL, + which means that no chmod'ing or directory creation shall + occur. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyFixLt.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyFixLt.txt index e0fa378ea8..568cbf3b32 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyFixLt.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyFixLt.txt @@ -1,18 +1,18 @@ -Core.AggressivelyFixLt -TYPE: bool -VERSION: 2.1.0 -DEFAULT: true ---DESCRIPTION-- -

- This directive enables aggressive pre-filter fixes HTML Purifier can - perform in order to ensure that open angled-brackets do not get killed - during parsing stage. Enabling this will result in two preg_replace_callback - calls and at least two preg_replace calls for every HTML document parsed; - if your users make very well-formed HTML, you can set this directive false. - This has no effect when DirectLex is used. -

-

- Notice: This directive's default turned from false to true - in HTML Purifier 3.2.0. -

---# vim: et sw=4 sts=4 +Core.AggressivelyFixLt +TYPE: bool +VERSION: 2.1.0 +DEFAULT: true +--DESCRIPTION-- +

+ This directive enables aggressive pre-filter fixes HTML Purifier can + perform in order to ensure that open angled-brackets do not get killed + during parsing stage. Enabling this will result in two preg_replace_callback + calls and at least two preg_replace calls for every HTML document parsed; + if your users make very well-formed HTML, you can set this directive false. + This has no effect when DirectLex is used. +

+

+ Notice: This directive's default turned from false to true + in HTML Purifier 3.2.0. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyRemoveScript.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyRemoveScript.txt index fb140b69db..b2b6ab1496 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyRemoveScript.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyRemoveScript.txt @@ -1,16 +1,16 @@ -Core.AggressivelyRemoveScript -TYPE: bool -VERSION: 4.9.0 -DEFAULT: true ---DESCRIPTION-- -

- This directive enables aggressive pre-filter removal of - script tags. This is not necessary for security, - but it can help work around a bug in libxml where embedded - HTML elements inside script sections cause the parser to - choke. To revert to pre-4.9.0 behavior, set this to false. - This directive has no effect if %Core.Trusted is true, - %Core.RemoveScriptContents is false, or %Core.HiddenElements - does not contain script. -

---# vim: et sw=4 sts=4 +Core.AggressivelyRemoveScript +TYPE: bool +VERSION: 4.9.0 +DEFAULT: true +--DESCRIPTION-- +

+ This directive enables aggressive pre-filter removal of + script tags. This is not necessary for security, + but it can help work around a bug in libxml where embedded + HTML elements inside script sections cause the parser to + choke. To revert to pre-4.9.0 behavior, set this to false. + This directive has no effect if %Core.Trusted is true, + %Core.RemoveScriptContents is false, or %Core.HiddenElements + does not contain script. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.AllowHostnameUnderscore.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.AllowHostnameUnderscore.txt index 405d36f174..2c910cc7de 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.AllowHostnameUnderscore.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.AllowHostnameUnderscore.txt @@ -1,16 +1,16 @@ -Core.AllowHostnameUnderscore -TYPE: bool -VERSION: 4.6.0 -DEFAULT: false ---DESCRIPTION-- -

- By RFC 1123, underscores are not permitted in host names. - (This is in contrast to the specification for DNS, RFC - 2181, which allows underscores.) - However, most browsers do the right thing when faced with - an underscore in the host name, and so some poorly written - websites are written with the expectation this should work. - Setting this parameter to true relaxes our allowed character - check so that underscores are permitted. -

---# vim: et sw=4 sts=4 +Core.AllowHostnameUnderscore +TYPE: bool +VERSION: 4.6.0 +DEFAULT: false +--DESCRIPTION-- +

+ By RFC 1123, underscores are not permitted in host names. + (This is in contrast to the specification for DNS, RFC + 2181, which allows underscores.) + However, most browsers do the right thing when faced with + an underscore in the host name, and so some poorly written + websites are written with the expectation this should work. + Setting this parameter to true relaxes our allowed character + check so that underscores are permitted. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.AllowParseManyTags.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.AllowParseManyTags.txt new file mode 100644 index 0000000000..06278f82a1 --- /dev/null +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.AllowParseManyTags.txt @@ -0,0 +1,12 @@ +Core.AllowParseManyTags +TYPE: bool +DEFAULT: false +VERSION: 4.10.1 +--DESCRIPTION-- +

+ This directive allows parsing of many nested tags. + If you set true, relaxes any hardcoded limit from the parser. + However, in that case it may cause a Dos attack. + Be careful when enabling it. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.CollectErrors.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.CollectErrors.txt index c6ea06990f..d7317911fa 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.CollectErrors.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.CollectErrors.txt @@ -1,12 +1,12 @@ -Core.CollectErrors -TYPE: bool -VERSION: 2.0.0 -DEFAULT: false ---DESCRIPTION-- - -Whether or not to collect errors found while filtering the document. This -is a useful way to give feedback to your users. Warning: -Currently this feature is very patchy and experimental, with lots of -possible error messages not yet implemented. It will not cause any -problems, but it may not help your users either. ---# vim: et sw=4 sts=4 +Core.CollectErrors +TYPE: bool +VERSION: 2.0.0 +DEFAULT: false +--DESCRIPTION-- + +Whether or not to collect errors found while filtering the document. This +is a useful way to give feedback to your users. Warning: +Currently this feature is very patchy and experimental, with lots of +possible error messages not yet implemented. It will not cause any +problems, but it may not help your users either. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.ColorKeywords.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.ColorKeywords.txt index f782398253..a75844cd58 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.ColorKeywords.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.ColorKeywords.txt @@ -1,29 +1,160 @@ -Core.ColorKeywords -TYPE: hash -VERSION: 2.0.0 ---DEFAULT-- -array ( - 'maroon' => '#800000', - 'red' => '#FF0000', - 'orange' => '#FFA500', - 'yellow' => '#FFFF00', - 'olive' => '#808000', - 'purple' => '#800080', - 'fuchsia' => '#FF00FF', - 'white' => '#FFFFFF', - 'lime' => '#00FF00', - 'green' => '#008000', - 'navy' => '#000080', - 'blue' => '#0000FF', - 'aqua' => '#00FFFF', - 'teal' => '#008080', - 'black' => '#000000', - 'silver' => '#C0C0C0', - 'gray' => '#808080', -) ---DESCRIPTION-- - -Lookup array of color names to six digit hexadecimal number corresponding -to color, with preceding hash mark. Used when parsing colors. The lookup -is done in a case-insensitive manner. ---# vim: et sw=4 sts=4 +Core.ColorKeywords +TYPE: hash +VERSION: 2.0.0 +--DEFAULT-- +array ( + 'aliceblue' => '#F0F8FF', + 'antiquewhite' => '#FAEBD7', + 'aqua' => '#00FFFF', + 'aquamarine' => '#7FFFD4', + 'azure' => '#F0FFFF', + 'beige' => '#F5F5DC', + 'bisque' => '#FFE4C4', + 'black' => '#000000', + 'blanchedalmond' => '#FFEBCD', + 'blue' => '#0000FF', + 'blueviolet' => '#8A2BE2', + 'brown' => '#A52A2A', + 'burlywood' => '#DEB887', + 'cadetblue' => '#5F9EA0', + 'chartreuse' => '#7FFF00', + 'chocolate' => '#D2691E', + 'coral' => '#FF7F50', + 'cornflowerblue' => '#6495ED', + 'cornsilk' => '#FFF8DC', + 'crimson' => '#DC143C', + 'cyan' => '#00FFFF', + 'darkblue' => '#00008B', + 'darkcyan' => '#008B8B', + 'darkgoldenrod' => '#B8860B', + 'darkgray' => '#A9A9A9', + 'darkgrey' => '#A9A9A9', + 'darkgreen' => '#006400', + 'darkkhaki' => '#BDB76B', + 'darkmagenta' => '#8B008B', + 'darkolivegreen' => '#556B2F', + 'darkorange' => '#FF8C00', + 'darkorchid' => '#9932CC', + 'darkred' => '#8B0000', + 'darksalmon' => '#E9967A', + 'darkseagreen' => '#8FBC8F', + 'darkslateblue' => '#483D8B', + 'darkslategray' => '#2F4F4F', + 'darkslategrey' => '#2F4F4F', + 'darkturquoise' => '#00CED1', + 'darkviolet' => '#9400D3', + 'deeppink' => '#FF1493', + 'deepskyblue' => '#00BFFF', + 'dimgray' => '#696969', + 'dimgrey' => '#696969', + 'dodgerblue' => '#1E90FF', + 'firebrick' => '#B22222', + 'floralwhite' => '#FFFAF0', + 'forestgreen' => '#228B22', + 'fuchsia' => '#FF00FF', + 'gainsboro' => '#DCDCDC', + 'ghostwhite' => '#F8F8FF', + 'gold' => '#FFD700', + 'goldenrod' => '#DAA520', + 'gray' => '#808080', + 'grey' => '#808080', + 'green' => '#008000', + 'greenyellow' => '#ADFF2F', + 'honeydew' => '#F0FFF0', + 'hotpink' => '#FF69B4', + 'indianred' => '#CD5C5C', + 'indigo' => '#4B0082', + 'ivory' => '#FFFFF0', + 'khaki' => '#F0E68C', + 'lavender' => '#E6E6FA', + 'lavenderblush' => '#FFF0F5', + 'lawngreen' => '#7CFC00', + 'lemonchiffon' => '#FFFACD', + 'lightblue' => '#ADD8E6', + 'lightcoral' => '#F08080', + 'lightcyan' => '#E0FFFF', + 'lightgoldenrodyellow' => '#FAFAD2', + 'lightgray' => '#D3D3D3', + 'lightgrey' => '#D3D3D3', + 'lightgreen' => '#90EE90', + 'lightpink' => '#FFB6C1', + 'lightsalmon' => '#FFA07A', + 'lightseagreen' => '#20B2AA', + 'lightskyblue' => '#87CEFA', + 'lightslategray' => '#778899', + 'lightslategrey' => '#778899', + 'lightsteelblue' => '#B0C4DE', + 'lightyellow' => '#FFFFE0', + 'lime' => '#00FF00', + 'limegreen' => '#32CD32', + 'linen' => '#FAF0E6', + 'magenta' => '#FF00FF', + 'maroon' => '#800000', + 'mediumaquamarine' => '#66CDAA', + 'mediumblue' => '#0000CD', + 'mediumorchid' => '#BA55D3', + 'mediumpurple' => '#9370DB', + 'mediumseagreen' => '#3CB371', + 'mediumslateblue' => '#7B68EE', + 'mediumspringgreen' => '#00FA9A', + 'mediumturquoise' => '#48D1CC', + 'mediumvioletred' => '#C71585', + 'midnightblue' => '#191970', + 'mintcream' => '#F5FFFA', + 'mistyrose' => '#FFE4E1', + 'moccasin' => '#FFE4B5', + 'navajowhite' => '#FFDEAD', + 'navy' => '#000080', + 'oldlace' => '#FDF5E6', + 'olive' => '#808000', + 'olivedrab' => '#6B8E23', + 'orange' => '#FFA500', + 'orangered' => '#FF4500', + 'orchid' => '#DA70D6', + 'palegoldenrod' => '#EEE8AA', + 'palegreen' => '#98FB98', + 'paleturquoise' => '#AFEEEE', + 'palevioletred' => '#DB7093', + 'papayawhip' => '#FFEFD5', + 'peachpuff' => '#FFDAB9', + 'peru' => '#CD853F', + 'pink' => '#FFC0CB', + 'plum' => '#DDA0DD', + 'powderblue' => '#B0E0E6', + 'purple' => '#800080', + 'rebeccapurple' => '#663399', + 'red' => '#FF0000', + 'rosybrown' => '#BC8F8F', + 'royalblue' => '#4169E1', + 'saddlebrown' => '#8B4513', + 'salmon' => '#FA8072', + 'sandybrown' => '#F4A460', + 'seagreen' => '#2E8B57', + 'seashell' => '#FFF5EE', + 'sienna' => '#A0522D', + 'silver' => '#C0C0C0', + 'skyblue' => '#87CEEB', + 'slateblue' => '#6A5ACD', + 'slategray' => '#708090', + 'slategrey' => '#708090', + 'snow' => '#FFFAFA', + 'springgreen' => '#00FF7F', + 'steelblue' => '#4682B4', + 'tan' => '#D2B48C', + 'teal' => '#008080', + 'thistle' => '#D8BFD8', + 'tomato' => '#FF6347', + 'turquoise' => '#40E0D0', + 'violet' => '#EE82EE', + 'wheat' => '#F5DEB3', + 'white' => '#FFFFFF', + 'whitesmoke' => '#F5F5F5', + 'yellow' => '#FFFF00', + 'yellowgreen' => '#9ACD32' +) +--DESCRIPTION-- + +Lookup array of color names to six digit hexadecimal number corresponding +to color, with preceding hash mark. Used when parsing colors. The lookup +is done in a case-insensitive manner. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt index 656d3783a8..64b114fce2 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt @@ -1,14 +1,14 @@ -Core.ConvertDocumentToFragment -TYPE: bool -DEFAULT: true ---DESCRIPTION-- - -This parameter determines whether or not the filter should convert -input that is a full document with html and body tags to a fragment -of just the contents of a body tag. This parameter is simply something -HTML Purifier can do during an edge-case: for most inputs, this -processing is not necessary. - ---ALIASES-- -Core.AcceptFullDocuments ---# vim: et sw=4 sts=4 +Core.ConvertDocumentToFragment +TYPE: bool +DEFAULT: true +--DESCRIPTION-- + +This parameter determines whether or not the filter should convert +input that is a full document with html and body tags to a fragment +of just the contents of a body tag. This parameter is simply something +HTML Purifier can do during an edge-case: for most inputs, this +processing is not necessary. + +--ALIASES-- +Core.AcceptFullDocuments +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.DirectLexLineNumberSyncInterval.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.DirectLexLineNumberSyncInterval.txt index 2f54e462a1..36f16e07ea 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.DirectLexLineNumberSyncInterval.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.DirectLexLineNumberSyncInterval.txt @@ -1,17 +1,17 @@ -Core.DirectLexLineNumberSyncInterval -TYPE: int -VERSION: 2.0.0 -DEFAULT: 0 ---DESCRIPTION-- - -

- Specifies the number of tokens the DirectLex line number tracking - implementations should process before attempting to resyncronize the - current line count by manually counting all previous new-lines. When - at 0, this functionality is disabled. Lower values will decrease - performance, and this is only strictly necessary if the counting - algorithm is buggy (in which case you should report it as a bug). - This has no effect when %Core.MaintainLineNumbers is disabled or DirectLex is - not being used. -

---# vim: et sw=4 sts=4 +Core.DirectLexLineNumberSyncInterval +TYPE: int +VERSION: 2.0.0 +DEFAULT: 0 +--DESCRIPTION-- + +

+ Specifies the number of tokens the DirectLex line number tracking + implementations should process before attempting to resyncronize the + current line count by manually counting all previous new-lines. When + at 0, this functionality is disabled. Lower values will decrease + performance, and this is only strictly necessary if the counting + algorithm is buggy (in which case you should report it as a bug). + This has no effect when %Core.MaintainLineNumbers is disabled or DirectLex is + not being used. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.DisableExcludes.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.DisableExcludes.txt index 3c63c923c8..1cd4c2c964 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.DisableExcludes.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.DisableExcludes.txt @@ -1,14 +1,14 @@ -Core.DisableExcludes -TYPE: bool -DEFAULT: false -VERSION: 4.5.0 ---DESCRIPTION-- -

- This directive disables SGML-style exclusions, e.g. the exclusion of - <object> in any descendant of a - <pre> tag. Disabling excludes will allow some - invalid documents to pass through HTML Purifier, but HTML Purifier - will also be less likely to accidentally remove large documents during - processing. -

---# vim: et sw=4 sts=4 +Core.DisableExcludes +TYPE: bool +DEFAULT: false +VERSION: 4.5.0 +--DESCRIPTION-- +

+ This directive disables SGML-style exclusions, e.g. the exclusion of + <object> in any descendant of a + <pre> tag. Disabling excludes will allow some + invalid documents to pass through HTML Purifier, but HTML Purifier + will also be less likely to accidentally remove large documents during + processing. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.EnableIDNA.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.EnableIDNA.txt index 7f498e7e7a..ce243c35dc 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.EnableIDNA.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.EnableIDNA.txt @@ -1,9 +1,9 @@ -Core.EnableIDNA -TYPE: bool -DEFAULT: false -VERSION: 4.4.0 ---DESCRIPTION-- -Allows international domain names in URLs. This configuration option -requires the PEAR Net_IDNA2 module to be installed. It operates by -punycoding any internationalized host names for maximum portability. ---# vim: et sw=4 sts=4 +Core.EnableIDNA +TYPE: bool +DEFAULT: false +VERSION: 4.4.0 +--DESCRIPTION-- +Allows international domain names in URLs. This configuration option +requires the PEAR Net_IDNA2 module to be installed. It operates by +punycoding any internationalized host names for maximum portability. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.Encoding.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.Encoding.txt index 89e2ae34b1..8bfb47c3ac 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.Encoding.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.Encoding.txt @@ -1,15 +1,15 @@ -Core.Encoding -TYPE: istring -DEFAULT: 'utf-8' ---DESCRIPTION-- -If for some reason you are unable to convert all webpages to UTF-8, you can -use this directive as a stop-gap compatibility change to let HTML Purifier -deal with non UTF-8 input. This technique has notable deficiencies: -absolutely no characters outside of the selected character encoding will be -preserved, not even the ones that have been ampersand escaped (this is due -to a UTF-8 specific feature that automatically resolves all -entities), making it pretty useless for anything except the most I18N-blind -applications, although %Core.EscapeNonASCIICharacters offers fixes this -trouble with another tradeoff. This directive only accepts ISO-8859-1 if -iconv is not enabled. ---# vim: et sw=4 sts=4 +Core.Encoding +TYPE: istring +DEFAULT: 'utf-8' +--DESCRIPTION-- +If for some reason you are unable to convert all webpages to UTF-8, you can +use this directive as a stop-gap compatibility change to let HTML Purifier +deal with non UTF-8 input. This technique has notable deficiencies: +absolutely no characters outside of the selected character encoding will be +preserved, not even the ones that have been ampersand escaped (this is due +to a UTF-8 specific feature that automatically resolves all +entities), making it pretty useless for anything except the most I18N-blind +applications, although %Core.EscapeNonASCIICharacters offers fixes this +trouble with another tradeoff. This directive only accepts ISO-8859-1 if +iconv is not enabled. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidChildren.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidChildren.txt index 1cc3fcda2f..a3881be75c 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidChildren.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidChildren.txt @@ -1,12 +1,12 @@ -Core.EscapeInvalidChildren -TYPE: bool -DEFAULT: false ---DESCRIPTION-- -

Warning: this configuration option is no longer does anything as of 4.6.0.

- -

When true, a child is found that is not allowed in the context of the -parent element will be transformed into text as if it were ASCII. When -false, that element and all internal tags will be dropped, though text will -be preserved. There is no option for dropping the element but preserving -child nodes.

---# vim: et sw=4 sts=4 +Core.EscapeInvalidChildren +TYPE: bool +DEFAULT: false +--DESCRIPTION-- +

Warning: this configuration option is no longer does anything as of 4.6.0.

+ +

When true, a child is found that is not allowed in the context of the +parent element will be transformed into text as if it were ASCII. When +false, that element and all internal tags will be dropped, though text will +be preserved. There is no option for dropping the element but preserving +child nodes.

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidTags.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidTags.txt index 299775fab2..a7a5b249bb 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidTags.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidTags.txt @@ -1,7 +1,7 @@ -Core.EscapeInvalidTags -TYPE: bool -DEFAULT: false ---DESCRIPTION-- -When true, invalid tags will be written back to the document as plain text. -Otherwise, they are silently dropped. ---# vim: et sw=4 sts=4 +Core.EscapeInvalidTags +TYPE: bool +DEFAULT: false +--DESCRIPTION-- +When true, invalid tags will be written back to the document as plain text. +Otherwise, they are silently dropped. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt index f50db2f92a..abb499948a 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt @@ -1,13 +1,13 @@ -Core.EscapeNonASCIICharacters -TYPE: bool -VERSION: 1.4.0 -DEFAULT: false ---DESCRIPTION-- -This directive overcomes a deficiency in %Core.Encoding by blindly -converting all non-ASCII characters into decimal numeric entities before -converting it to its native encoding. This means that even characters that -can be expressed in the non-UTF-8 encoding will be entity-ized, which can -be a real downer for encodings like Big5. It also assumes that the ASCII -repetoire is available, although this is the case for almost all encodings. -Anyway, use UTF-8! ---# vim: et sw=4 sts=4 +Core.EscapeNonASCIICharacters +TYPE: bool +VERSION: 1.4.0 +DEFAULT: false +--DESCRIPTION-- +This directive overcomes a deficiency in %Core.Encoding by blindly +converting all non-ASCII characters into decimal numeric entities before +converting it to its native encoding. This means that even characters that +can be expressed in the non-UTF-8 encoding will be entity-ized, which can +be a real downer for encodings like Big5. It also assumes that the ASCII +repetoire is available, although this is the case for almost all encodings. +Anyway, use UTF-8! +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.HiddenElements.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.HiddenElements.txt index c337e47fca..915391edb7 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.HiddenElements.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.HiddenElements.txt @@ -1,19 +1,19 @@ -Core.HiddenElements -TYPE: lookup ---DEFAULT-- -array ( - 'script' => true, - 'style' => true, -) ---DESCRIPTION-- - -

- This directive is a lookup array of elements which should have their - contents removed when they are not allowed by the HTML definition. - For example, the contents of a script tag are not - normally shown in a document, so if script tags are to be removed, - their contents should be removed to. This is opposed to a b - tag, which defines some presentational changes but does not hide its - contents. -

---# vim: et sw=4 sts=4 +Core.HiddenElements +TYPE: lookup +--DEFAULT-- +array ( + 'script' => true, + 'style' => true, +) +--DESCRIPTION-- + +

+ This directive is a lookup array of elements which should have their + contents removed when they are not allowed by the HTML definition. + For example, the contents of a script tag are not + normally shown in a document, so if script tags are to be removed, + their contents should be removed to. This is opposed to a b + tag, which defines some presentational changes but does not hide its + contents. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.Language.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.Language.txt index ed1f39b5f8..233fca14f8 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.Language.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.Language.txt @@ -1,10 +1,10 @@ -Core.Language -TYPE: string -VERSION: 2.0.0 -DEFAULT: 'en' ---DESCRIPTION-- - -ISO 639 language code for localizable things in HTML Purifier to use, -which is mainly error reporting. There is currently only an English (en) -translation, so this directive is currently useless. ---# vim: et sw=4 sts=4 +Core.Language +TYPE: string +VERSION: 2.0.0 +DEFAULT: 'en' +--DESCRIPTION-- + +ISO 639 language code for localizable things in HTML Purifier to use, +which is mainly error reporting. There is currently only an English (en) +translation, so this directive is currently useless. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.LegacyEntityDecoder.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.LegacyEntityDecoder.txt index 81d9ae4dcf..392b436493 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.LegacyEntityDecoder.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.LegacyEntityDecoder.txt @@ -1,36 +1,36 @@ -Core.LegacyEntityDecoder -TYPE: bool -VERSION: 4.9.0 -DEFAULT: false ---DESCRIPTION-- -

- Prior to HTML Purifier 4.9.0, entities were decoded by performing - a global search replace for all entities whose decoded versions - did not have special meanings under HTML, and replaced them with - their decoded versions. We would match all entities, even if they did - not have a trailing semicolon, but only if there weren't any trailing - alphanumeric characters. -

- - - - - - -
OriginalTextAttribute
&yen;¥¥
&yen¥¥
&yena&yena&yena
&yen=¥=¥=
-

- In HTML Purifier 4.9.0, we changed the behavior of entity parsing - to match entities that had missing trailing semicolons in less - cases, to more closely match HTML5 parsing behavior: -

- - - - - - -
OriginalTextAttribute
&yen;¥¥
&yen¥¥
&yena¥a&yena
&yen=¥=&yen=
-

- This flag reverts back to pre-HTML Purifier 4.9.0 behavior. -

---# vim: et sw=4 sts=4 +Core.LegacyEntityDecoder +TYPE: bool +VERSION: 4.9.0 +DEFAULT: false +--DESCRIPTION-- +

+ Prior to HTML Purifier 4.9.0, entities were decoded by performing + a global search replace for all entities whose decoded versions + did not have special meanings under HTML, and replaced them with + their decoded versions. We would match all entities, even if they did + not have a trailing semicolon, but only if there weren't any trailing + alphanumeric characters. +

+ + + + + + +
OriginalTextAttribute
&yen;¥¥
&yen¥¥
&yena&yena&yena
&yen=¥=¥=
+

+ In HTML Purifier 4.9.0, we changed the behavior of entity parsing + to match entities that had missing trailing semicolons in less + cases, to more closely match HTML5 parsing behavior: +

+ + + + + + +
OriginalTextAttribute
&yen;¥¥
&yen¥¥
&yena¥a&yena
&yen=¥=&yen=
+

+ This flag reverts back to pre-HTML Purifier 4.9.0 behavior. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.LexerImpl.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.LexerImpl.txt index e11c0152c2..8983e2cca9 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.LexerImpl.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.LexerImpl.txt @@ -1,34 +1,34 @@ -Core.LexerImpl -TYPE: mixed/null -VERSION: 2.0.0 -DEFAULT: NULL ---DESCRIPTION-- - -

- This parameter determines what lexer implementation can be used. The - valid values are: -

-
-
null
-
- Recommended, the lexer implementation will be auto-detected based on - your PHP-version and configuration. -
-
string lexer identifier
-
- This is a slim way of manually overridding the implementation. - Currently recognized values are: DOMLex (the default PHP5 -implementation) - and DirectLex (the default PHP4 implementation). Only use this if - you know what you are doing: usually, the auto-detection will - manage things for cases you aren't even aware of. -
-
object lexer instance
-
- Super-advanced: you can specify your own, custom, implementation that - implements the interface defined by HTMLPurifier_Lexer. - I may remove this option simply because I don't expect anyone - to use it. -
-
---# vim: et sw=4 sts=4 +Core.LexerImpl +TYPE: mixed/null +VERSION: 2.0.0 +DEFAULT: NULL +--DESCRIPTION-- + +

+ This parameter determines what lexer implementation can be used. The + valid values are: +

+
+
null
+
+ Recommended, the lexer implementation will be auto-detected based on + your PHP-version and configuration. +
+
string lexer identifier
+
+ This is a slim way of manually overridding the implementation. + Currently recognized values are: DOMLex (the default PHP5 +implementation) + and DirectLex (the default PHP4 implementation). Only use this if + you know what you are doing: usually, the auto-detection will + manage things for cases you aren't even aware of. +
+
object lexer instance
+
+ Super-advanced: you can specify your own, custom, implementation that + implements the interface defined by HTMLPurifier_Lexer. + I may remove this option simply because I don't expect anyone + to use it. +
+
+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.MaintainLineNumbers.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.MaintainLineNumbers.txt index 838f10f61c..eb841a7597 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.MaintainLineNumbers.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.MaintainLineNumbers.txt @@ -1,16 +1,16 @@ -Core.MaintainLineNumbers -TYPE: bool/null -VERSION: 2.0.0 -DEFAULT: NULL ---DESCRIPTION-- - -

- If true, HTML Purifier will add line number information to all tokens. - This is useful when error reporting is turned on, but can result in - significant performance degradation and should not be used when - unnecessary. This directive must be used with the DirectLex lexer, - as the DOMLex lexer does not (yet) support this functionality. - If the value is null, an appropriate value will be selected based - on other configuration. -

---# vim: et sw=4 sts=4 +Core.MaintainLineNumbers +TYPE: bool/null +VERSION: 2.0.0 +DEFAULT: NULL +--DESCRIPTION-- + +

+ If true, HTML Purifier will add line number information to all tokens. + This is useful when error reporting is turned on, but can result in + significant performance degradation and should not be used when + unnecessary. This directive must be used with the DirectLex lexer, + as the DOMLex lexer does not (yet) support this functionality. + If the value is null, an appropriate value will be selected based + on other configuration. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.NormalizeNewlines.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.NormalizeNewlines.txt index 94a88600de..d77f5360d7 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.NormalizeNewlines.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.NormalizeNewlines.txt @@ -1,11 +1,11 @@ -Core.NormalizeNewlines -TYPE: bool -VERSION: 4.2.0 -DEFAULT: true ---DESCRIPTION-- -

- Whether or not to normalize newlines to the operating - system default. When false, HTML Purifier - will attempt to preserve mixed newline files. -

---# vim: et sw=4 sts=4 +Core.NormalizeNewlines +TYPE: bool +VERSION: 4.2.0 +DEFAULT: true +--DESCRIPTION-- +

+ Whether or not to normalize newlines to the operating + system default. When false, HTML Purifier + will attempt to preserve mixed newline files. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveInvalidImg.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveInvalidImg.txt index 704ac56c85..4070c2a0de 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveInvalidImg.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveInvalidImg.txt @@ -1,12 +1,12 @@ -Core.RemoveInvalidImg -TYPE: bool -DEFAULT: true -VERSION: 1.3.0 ---DESCRIPTION-- - -

- This directive enables pre-emptive URI checking in img - tags, as the attribute validation strategy is not authorized to - remove elements from the document. Revert to pre-1.3.0 behavior by setting to false. -

---# vim: et sw=4 sts=4 +Core.RemoveInvalidImg +TYPE: bool +DEFAULT: true +VERSION: 1.3.0 +--DESCRIPTION-- + +

+ This directive enables pre-emptive URI checking in img + tags, as the attribute validation strategy is not authorized to + remove elements from the document. Revert to pre-1.3.0 behavior by setting to false. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveProcessingInstructions.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveProcessingInstructions.txt index ed6f13425e..3397d9f71f 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveProcessingInstructions.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveProcessingInstructions.txt @@ -1,11 +1,11 @@ -Core.RemoveProcessingInstructions -TYPE: bool -VERSION: 4.2.0 -DEFAULT: false ---DESCRIPTION-- -Instead of escaping processing instructions in the form <? ... -?>, remove it out-right. This may be useful if the HTML -you are validating contains XML processing instruction gunk, however, -it can also be user-unfriendly for people attempting to post PHP -snippets. ---# vim: et sw=4 sts=4 +Core.RemoveProcessingInstructions +TYPE: bool +VERSION: 4.2.0 +DEFAULT: false +--DESCRIPTION-- +Instead of escaping processing instructions in the form <? ... +?>, remove it out-right. This may be useful if the HTML +you are validating contains XML processing instruction gunk, however, +it can also be user-unfriendly for people attempting to post PHP +snippets. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveScriptContents.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveScriptContents.txt index efbe994c28..a4cd966df8 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveScriptContents.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveScriptContents.txt @@ -1,12 +1,12 @@ -Core.RemoveScriptContents -TYPE: bool/null -DEFAULT: NULL -VERSION: 2.0.0 -DEPRECATED-VERSION: 2.1.0 -DEPRECATED-USE: Core.HiddenElements ---DESCRIPTION-- -

- This directive enables HTML Purifier to remove not only script tags - but all of their contents. -

---# vim: et sw=4 sts=4 +Core.RemoveScriptContents +TYPE: bool/null +DEFAULT: NULL +VERSION: 2.0.0 +DEPRECATED-VERSION: 2.1.0 +DEPRECATED-USE: Core.HiddenElements +--DESCRIPTION-- +

+ This directive enables HTML Purifier to remove not only script tags + but all of their contents. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Filter.Custom.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Filter.Custom.txt index 861ae66c3a..3db50ef204 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Filter.Custom.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Filter.Custom.txt @@ -1,11 +1,11 @@ -Filter.Custom -TYPE: list -VERSION: 3.1.0 -DEFAULT: array() ---DESCRIPTION-- -

- This directive can be used to add custom filters; it is nearly the - equivalent of the now deprecated HTMLPurifier->addFilter() - method. Specify an array of concrete implementations. -

---# vim: et sw=4 sts=4 +Filter.Custom +TYPE: list +VERSION: 3.1.0 +DEFAULT: array() +--DESCRIPTION-- +

+ This directive can be used to add custom filters; it is nearly the + equivalent of the now deprecated HTMLPurifier->addFilter() + method. Specify an array of concrete implementations. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Escaping.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Escaping.txt index 69602635ea..16829bcda0 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Escaping.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Escaping.txt @@ -1,14 +1,14 @@ -Filter.ExtractStyleBlocks.Escaping -TYPE: bool -VERSION: 3.0.0 -DEFAULT: true -ALIASES: Filter.ExtractStyleBlocksEscaping, FilterParam.ExtractStyleBlocksEscaping ---DESCRIPTION-- - -

- Whether or not to escape the dangerous characters <, > and & - as \3C, \3E and \26, respectively. This is can be safely set to false - if the contents of StyleBlocks will be placed in an external stylesheet, - where there is no risk of it being interpreted as HTML. -

---# vim: et sw=4 sts=4 +Filter.ExtractStyleBlocks.Escaping +TYPE: bool +VERSION: 3.0.0 +DEFAULT: true +ALIASES: Filter.ExtractStyleBlocksEscaping, FilterParam.ExtractStyleBlocksEscaping +--DESCRIPTION-- + +

+ Whether or not to escape the dangerous characters <, > and & + as \3C, \3E and \26, respectively. This is can be safely set to false + if the contents of StyleBlocks will be placed in an external stylesheet, + where there is no risk of it being interpreted as HTML. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Scope.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Scope.txt index baa81ae062..7f95f54d12 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Scope.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Scope.txt @@ -1,29 +1,29 @@ -Filter.ExtractStyleBlocks.Scope -TYPE: string/null -VERSION: 3.0.0 -DEFAULT: NULL -ALIASES: Filter.ExtractStyleBlocksScope, FilterParam.ExtractStyleBlocksScope ---DESCRIPTION-- - -

- If you would like users to be able to define external stylesheets, but - only allow them to specify CSS declarations for a specific node and - prevent them from fiddling with other elements, use this directive. - It accepts any valid CSS selector, and will prepend this to any - CSS declaration extracted from the document. For example, if this - directive is set to #user-content and a user uses the - selector a:hover, the final selector will be - #user-content a:hover. -

-

- The comma shorthand may be used; consider the above example, with - #user-content, #user-content2, the final selector will - be #user-content a:hover, #user-content2 a:hover. -

-

- Warning: It is possible for users to bypass this measure - using a naughty + selector. This is a bug in CSS Tidy 1.3, not HTML - Purifier, and I am working to get it fixed. Until then, HTML Purifier - performs a basic check to prevent this. -

---# vim: et sw=4 sts=4 +Filter.ExtractStyleBlocks.Scope +TYPE: string/null +VERSION: 3.0.0 +DEFAULT: NULL +ALIASES: Filter.ExtractStyleBlocksScope, FilterParam.ExtractStyleBlocksScope +--DESCRIPTION-- + +

+ If you would like users to be able to define external stylesheets, but + only allow them to specify CSS declarations for a specific node and + prevent them from fiddling with other elements, use this directive. + It accepts any valid CSS selector, and will prepend this to any + CSS declaration extracted from the document. For example, if this + directive is set to #user-content and a user uses the + selector a:hover, the final selector will be + #user-content a:hover. +

+

+ The comma shorthand may be used; consider the above example, with + #user-content, #user-content2, the final selector will + be #user-content a:hover, #user-content2 a:hover. +

+

+ Warning: It is possible for users to bypass this measure + using a naughty + selector. This is a bug in CSS Tidy 1.3, not HTML + Purifier, and I am working to get it fixed. Until then, HTML Purifier + performs a basic check to prevent this. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.TidyImpl.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.TidyImpl.txt index 3b7018917e..6c231b2d7f 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.TidyImpl.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.TidyImpl.txt @@ -1,16 +1,16 @@ -Filter.ExtractStyleBlocks.TidyImpl -TYPE: mixed/null -VERSION: 3.1.0 -DEFAULT: NULL -ALIASES: FilterParam.ExtractStyleBlocksTidyImpl ---DESCRIPTION-- -

- If left NULL, HTML Purifier will attempt to instantiate a csstidy - class to use for internal cleaning. This will usually be good enough. -

-

- However, for trusted user input, you can set this to false to - disable cleaning. In addition, you can supply your own concrete implementation - of Tidy's interface to use, although I don't know why you'd want to do that. -

---# vim: et sw=4 sts=4 +Filter.ExtractStyleBlocks.TidyImpl +TYPE: mixed/null +VERSION: 3.1.0 +DEFAULT: NULL +ALIASES: FilterParam.ExtractStyleBlocksTidyImpl +--DESCRIPTION-- +

+ If left NULL, HTML Purifier will attempt to instantiate a csstidy + class to use for internal cleaning. This will usually be good enough. +

+

+ However, for trusted user input, you can set this to false to + disable cleaning. In addition, you can supply your own concrete implementation + of Tidy's interface to use, although I don't know why you'd want to do that. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.txt index be0177d4e0..078d087417 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.txt @@ -1,74 +1,74 @@ -Filter.ExtractStyleBlocks -TYPE: bool -VERSION: 3.1.0 -DEFAULT: false -EXTERNAL: CSSTidy ---DESCRIPTION-- -

- This directive turns on the style block extraction filter, which removes - style blocks from input HTML, cleans them up with CSSTidy, - and places them in the StyleBlocks context variable, for further - use by you, usually to be placed in an external stylesheet, or a - style block in the head of your document. -

-

- Sample usage: -

-
';
-?>
-
-
-
-  Filter.ExtractStyleBlocks
-body {color:#F00;} Some text';
-
-    $config = HTMLPurifier_Config::createDefault();
-    $config->set('Filter', 'ExtractStyleBlocks', true);
-    $purifier = new HTMLPurifier($config);
-
-    $html = $purifier->purify($dirty);
-
-    // This implementation writes the stylesheets to the styles/ directory.
-    // You can also echo the styles inside the document, but it's a bit
-    // more difficult to make sure they get interpreted properly by
-    // browsers; try the usual CSS armoring techniques.
-    $styles = $purifier->context->get('StyleBlocks');
-    $dir = 'styles/';
-    if (!is_dir($dir)) mkdir($dir);
-    $hash = sha1($_GET['html']);
-    foreach ($styles as $i => $style) {
-        file_put_contents($name = $dir . $hash . "_$i");
-        echo '';
-    }
-?>
-
-
-  
- -
- - -]]>
-

- Warning: It is possible for a user to mount an - imagecrash attack using this CSS. Counter-measures are difficult; - it is not simply enough to limit the range of CSS lengths (using - relative lengths with many nesting levels allows for large values - to be attained without actually specifying them in the stylesheet), - and the flexible nature of selectors makes it difficult to selectively - disable lengths on image tags (HTML Purifier, however, does disable - CSS width and height in inline styling). There are probably two effective - counter measures: an explicit width and height set to auto in all - images in your document (unlikely) or the disabling of width and - height (somewhat reasonable). Whether or not these measures should be - used is left to the reader. -

---# vim: et sw=4 sts=4 +Filter.ExtractStyleBlocks +TYPE: bool +VERSION: 3.1.0 +DEFAULT: false +EXTERNAL: CSSTidy +--DESCRIPTION-- +

+ This directive turns on the style block extraction filter, which removes + style blocks from input HTML, cleans them up with CSSTidy, + and places them in the StyleBlocks context variable, for further + use by you, usually to be placed in an external stylesheet, or a + style block in the head of your document. +

+

+ Sample usage: +

+
';
+?>
+
+
+
+  Filter.ExtractStyleBlocks
+body {color:#F00;} Some text';
+
+    $config = HTMLPurifier_Config::createDefault();
+    $config->set('Filter', 'ExtractStyleBlocks', true);
+    $purifier = new HTMLPurifier($config);
+
+    $html = $purifier->purify($dirty);
+
+    // This implementation writes the stylesheets to the styles/ directory.
+    // You can also echo the styles inside the document, but it's a bit
+    // more difficult to make sure they get interpreted properly by
+    // browsers; try the usual CSS armoring techniques.
+    $styles = $purifier->context->get('StyleBlocks');
+    $dir = 'styles/';
+    if (!is_dir($dir)) mkdir($dir);
+    $hash = sha1($_GET['html']);
+    foreach ($styles as $i => $style) {
+        file_put_contents($name = $dir . $hash . "_$i");
+        echo '';
+    }
+?>
+
+
+  
+ +
+ + +]]>
+

+ Warning: It is possible for a user to mount an + imagecrash attack using this CSS. Counter-measures are difficult; + it is not simply enough to limit the range of CSS lengths (using + relative lengths with many nesting levels allows for large values + to be attained without actually specifying them in the stylesheet), + and the flexible nature of selectors makes it difficult to selectively + disable lengths on image tags (HTML Purifier, however, does disable + CSS width and height in inline styling). There are probably two effective + counter measures: an explicit width and height set to auto in all + images in your document (unlikely) or the disabling of width and + height (somewhat reasonable). Whether or not these measures should be + used is left to the reader. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Filter.YouTube.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Filter.YouTube.txt index 8822186685..321eaa2d80 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Filter.YouTube.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Filter.YouTube.txt @@ -1,16 +1,16 @@ -Filter.YouTube -TYPE: bool -VERSION: 3.1.0 -DEFAULT: false ---DESCRIPTION-- -

- Warning: Deprecated in favor of %HTML.SafeObject and - %Output.FlashCompat (turn both on to allow YouTube videos and other - Flash content). -

-

- This directive enables YouTube video embedding in HTML Purifier. Check - this document - on embedding videos for more information on what this filter does. -

---# vim: et sw=4 sts=4 +Filter.YouTube +TYPE: bool +VERSION: 3.1.0 +DEFAULT: false +--DESCRIPTION-- +

+ Warning: Deprecated in favor of %HTML.SafeObject and + %Output.FlashCompat (turn both on to allow YouTube videos and other + Flash content). +

+

+ This directive enables YouTube video embedding in HTML Purifier. Check + this document + on embedding videos for more information on what this filter does. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Allowed.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Allowed.txt index afd48a0d47..0b2c106da5 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Allowed.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Allowed.txt @@ -1,25 +1,25 @@ -HTML.Allowed -TYPE: itext/null -VERSION: 2.0.0 -DEFAULT: NULL ---DESCRIPTION-- - -

- This is a preferred convenience directive that combines - %HTML.AllowedElements and %HTML.AllowedAttributes. - Specify elements and attributes that are allowed using: - element1[attr1|attr2],element2.... For example, - if you would like to only allow paragraphs and links, specify - a[href],p. You can specify attributes that apply - to all elements using an asterisk, e.g. *[lang]. - You can also use newlines instead of commas to separate elements. -

-

- Warning: - All of the constraints on the component directives are still enforced. - The syntax is a subset of TinyMCE's valid_elements - whitelist: directly copy-pasting it here will probably result in - broken whitelists. If %HTML.AllowedElements or %HTML.AllowedAttributes - are set, this directive has no effect. -

---# vim: et sw=4 sts=4 +HTML.Allowed +TYPE: itext/null +VERSION: 2.0.0 +DEFAULT: NULL +--DESCRIPTION-- + +

+ This is a preferred convenience directive that combines + %HTML.AllowedElements and %HTML.AllowedAttributes. + Specify elements and attributes that are allowed using: + element1[attr1|attr2],element2.... For example, + if you would like to only allow paragraphs and links, specify + a[href],p. You can specify attributes that apply + to all elements using an asterisk, e.g. *[lang]. + You can also use newlines instead of commas to separate elements. +

+

+ Warning: + All of the constraints on the component directives are still enforced. + The syntax is a subset of TinyMCE's valid_elements + whitelist: directly copy-pasting it here will probably result in + broken whitelists. If %HTML.AllowedElements or %HTML.AllowedAttributes + are set, this directive has no effect. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedAttributes.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedAttributes.txt index 0e6ec54f33..fcf093f17d 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedAttributes.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedAttributes.txt @@ -1,19 +1,19 @@ -HTML.AllowedAttributes -TYPE: lookup/null -VERSION: 1.3.0 -DEFAULT: NULL ---DESCRIPTION-- - -

- If HTML Purifier's attribute set is unsatisfactory, overload it! - The syntax is "tag.attr" or "*.attr" for the global attributes - (style, id, class, dir, lang, xml:lang). -

-

- Warning: If another directive conflicts with the - elements here, that directive will win and override. For - example, %HTML.EnableAttrID will take precedence over *.id in this - directive. You must set that directive to true before you can use - IDs at all. -

---# vim: et sw=4 sts=4 +HTML.AllowedAttributes +TYPE: lookup/null +VERSION: 1.3.0 +DEFAULT: NULL +--DESCRIPTION-- + +

+ If HTML Purifier's attribute set is unsatisfactory, overload it! + The syntax is "tag.attr" or "*.attr" for the global attributes + (style, id, class, dir, lang, xml:lang). +

+

+ Warning: If another directive conflicts with the + elements here, that directive will win and override. For + example, %HTML.EnableAttrID will take precedence over *.id in this + directive. You must set that directive to true before you can use + IDs at all. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedComments.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedComments.txt index 8440bc39df..140e21423e 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedComments.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedComments.txt @@ -1,10 +1,10 @@ -HTML.AllowedComments -TYPE: lookup -VERSION: 4.4.0 -DEFAULT: array() ---DESCRIPTION-- -A whitelist which indicates what explicit comment bodies should be -allowed, modulo leading and trailing whitespace. See also %HTML.AllowedCommentsRegexp -(these directives are union'ed together, so a comment is considered -valid if any directive deems it valid.) ---# vim: et sw=4 sts=4 +HTML.AllowedComments +TYPE: lookup +VERSION: 4.4.0 +DEFAULT: array() +--DESCRIPTION-- +A whitelist which indicates what explicit comment bodies should be +allowed, modulo leading and trailing whitespace. See also %HTML.AllowedCommentsRegexp +(these directives are union'ed together, so a comment is considered +valid if any directive deems it valid.) +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedCommentsRegexp.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedCommentsRegexp.txt index b1e65beb16..f22e977d43 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedCommentsRegexp.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedCommentsRegexp.txt @@ -1,15 +1,15 @@ -HTML.AllowedCommentsRegexp -TYPE: string/null -VERSION: 4.4.0 -DEFAULT: NULL ---DESCRIPTION-- -A regexp, which if it matches the body of a comment, indicates that -it should be allowed. Trailing and leading spaces are removed prior -to running this regular expression. -Warning: Make sure you specify -correct anchor metacharacters ^regex$, otherwise you may accept -comments that you did not mean to! In particular, the regex /foo|bar/ -is probably not sufficiently strict, since it also allows foobar. -See also %HTML.AllowedComments (these directives are union'ed together, -so a comment is considered valid if any directive deems it valid.) ---# vim: et sw=4 sts=4 +HTML.AllowedCommentsRegexp +TYPE: string/null +VERSION: 4.4.0 +DEFAULT: NULL +--DESCRIPTION-- +A regexp, which if it matches the body of a comment, indicates that +it should be allowed. Trailing and leading spaces are removed prior +to running this regular expression. +Warning: Make sure you specify +correct anchor metacharacters ^regex$, otherwise you may accept +comments that you did not mean to! In particular, the regex /foo|bar/ +is probably not sufficiently strict, since it also allows foobar. +See also %HTML.AllowedComments (these directives are union'ed together, +so a comment is considered valid if any directive deems it valid.) +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt index ca3c13ddbf..1d3fa7907d 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt @@ -1,23 +1,23 @@ -HTML.AllowedElements -TYPE: lookup/null -VERSION: 1.3.0 -DEFAULT: NULL ---DESCRIPTION-- -

- If HTML Purifier's tag set is unsatisfactory for your needs, you can - overload it with your own list of tags to allow. If you change - this, you probably also want to change %HTML.AllowedAttributes; see - also %HTML.Allowed which lets you set allowed elements and - attributes at the same time. -

-

- If you attempt to allow an element that HTML Purifier does not know - about, HTML Purifier will raise an error. You will need to manually - tell HTML Purifier about this element by using the - advanced customization features. -

-

- Warning: If another directive conflicts with the - elements here, that directive will win and override. -

---# vim: et sw=4 sts=4 +HTML.AllowedElements +TYPE: lookup/null +VERSION: 1.3.0 +DEFAULT: NULL +--DESCRIPTION-- +

+ If HTML Purifier's tag set is unsatisfactory for your needs, you can + overload it with your own list of tags to allow. If you change + this, you probably also want to change %HTML.AllowedAttributes; see + also %HTML.Allowed which lets you set allowed elements and + attributes at the same time. +

+

+ If you attempt to allow an element that HTML Purifier does not know + about, HTML Purifier will raise an error. You will need to manually + tell HTML Purifier about this element by using the + advanced customization features. +

+

+ Warning: If another directive conflicts with the + elements here, that directive will win and override. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedModules.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedModules.txt index e373791a58..5a59a55c08 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedModules.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedModules.txt @@ -1,20 +1,20 @@ -HTML.AllowedModules -TYPE: lookup/null -VERSION: 2.0.0 -DEFAULT: NULL ---DESCRIPTION-- - -

- A doctype comes with a set of usual modules to use. Without having - to mucking about with the doctypes, you can quickly activate or - disable these modules by specifying which modules you wish to allow - with this directive. This is most useful for unit testing specific - modules, although end users may find it useful for their own ends. -

-

- If you specify a module that does not exist, the manager will silently - fail to use it, so be careful! User-defined modules are not affected - by this directive. Modules defined in %HTML.CoreModules are not - affected by this directive. -

---# vim: et sw=4 sts=4 +HTML.AllowedModules +TYPE: lookup/null +VERSION: 2.0.0 +DEFAULT: NULL +--DESCRIPTION-- + +

+ A doctype comes with a set of usual modules to use. Without having + to mucking about with the doctypes, you can quickly activate or + disable these modules by specifying which modules you wish to allow + with this directive. This is most useful for unit testing specific + modules, although end users may find it useful for their own ends. +

+

+ If you specify a module that does not exist, the manager will silently + fail to use it, so be careful! User-defined modules are not affected + by this directive. Modules defined in %HTML.CoreModules are not + affected by this directive. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Attr.Name.UseCDATA.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Attr.Name.UseCDATA.txt index 75d680ee1e..151fb7b826 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Attr.Name.UseCDATA.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Attr.Name.UseCDATA.txt @@ -1,11 +1,11 @@ -HTML.Attr.Name.UseCDATA -TYPE: bool -DEFAULT: false -VERSION: 4.0.0 ---DESCRIPTION-- -The W3C specification DTD defines the name attribute to be CDATA, not ID, due -to limitations of DTD. In certain documents, this relaxed behavior is desired, -whether it is to specify duplicate names, or to specify names that would be -illegal IDs (for example, names that begin with a digit.) Set this configuration -directive to true to use the relaxed parsing rules. ---# vim: et sw=4 sts=4 +HTML.Attr.Name.UseCDATA +TYPE: bool +DEFAULT: false +VERSION: 4.0.0 +--DESCRIPTION-- +The W3C specification DTD defines the name attribute to be CDATA, not ID, due +to limitations of DTD. In certain documents, this relaxed behavior is desired, +whether it is to specify duplicate names, or to specify names that would be +illegal IDs (for example, names that begin with a digit.) Set this configuration +directive to true to use the relaxed parsing rules. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt index f32b802c6d..45ae469ec9 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt @@ -1,18 +1,18 @@ -HTML.BlockWrapper -TYPE: string -VERSION: 1.3.0 -DEFAULT: 'p' ---DESCRIPTION-- - -

- String name of element to wrap inline elements that are inside a block - context. This only occurs in the children of blockquote in strict mode. -

-

- Example: by default value, - <blockquote>Foo</blockquote> would become - <blockquote><p>Foo</p></blockquote>. - The <p> tags can be replaced with whatever you desire, - as long as it is a block level element. -

---# vim: et sw=4 sts=4 +HTML.BlockWrapper +TYPE: string +VERSION: 1.3.0 +DEFAULT: 'p' +--DESCRIPTION-- + +

+ String name of element to wrap inline elements that are inside a block + context. This only occurs in the children of blockquote in strict mode. +

+

+ Example: by default value, + <blockquote>Foo</blockquote> would become + <blockquote><p>Foo</p></blockquote>. + The <p> tags can be replaced with whatever you desire, + as long as it is a block level element. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.CoreModules.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.CoreModules.txt index fc8e402058..5246188795 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.CoreModules.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.CoreModules.txt @@ -1,23 +1,23 @@ -HTML.CoreModules -TYPE: lookup -VERSION: 2.0.0 ---DEFAULT-- -array ( - 'Structure' => true, - 'Text' => true, - 'Hypertext' => true, - 'List' => true, - 'NonXMLCommonAttributes' => true, - 'XMLCommonAttributes' => true, - 'CommonAttributes' => true, -) ---DESCRIPTION-- - -

- Certain modularized doctypes (XHTML, namely), have certain modules - that must be included for the doctype to be an conforming document - type: put those modules here. By default, XHTML's core modules - are used. You can set this to a blank array to disable core module - protection, but this is not recommended. -

---# vim: et sw=4 sts=4 +HTML.CoreModules +TYPE: lookup +VERSION: 2.0.0 +--DEFAULT-- +array ( + 'Structure' => true, + 'Text' => true, + 'Hypertext' => true, + 'List' => true, + 'NonXMLCommonAttributes' => true, + 'XMLCommonAttributes' => true, + 'CommonAttributes' => true, +) +--DESCRIPTION-- + +

+ Certain modularized doctypes (XHTML, namely), have certain modules + that must be included for the doctype to be an conforming document + type: put those modules here. By default, XHTML's core modules + are used. You can set this to a blank array to disable core module + protection, but this is not recommended. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.CustomDoctype.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.CustomDoctype.txt index 187c0a0d5e..6ed70b599f 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.CustomDoctype.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.CustomDoctype.txt @@ -1,9 +1,9 @@ -HTML.CustomDoctype -TYPE: string/null -VERSION: 2.0.1 -DEFAULT: NULL ---DESCRIPTION-- - -A custom doctype for power-users who defined their own document -type. This directive only applies when %HTML.Doctype is blank. ---# vim: et sw=4 sts=4 +HTML.CustomDoctype +TYPE: string/null +VERSION: 2.0.1 +DEFAULT: NULL +--DESCRIPTION-- + +A custom doctype for power-users who defined their own document +type. This directive only applies when %HTML.Doctype is blank. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt index f5433e3f1f..103db754a2 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt @@ -1,33 +1,33 @@ -HTML.DefinitionID -TYPE: string/null -DEFAULT: NULL -VERSION: 2.0.0 ---DESCRIPTION-- - -

- Unique identifier for a custom-built HTML definition. If you edit - the raw version of the HTMLDefinition, introducing changes that the - configuration object does not reflect, you must specify this variable. - If you change your custom edits, you should change this directive, or - clear your cache. Example: -

-
-$config = HTMLPurifier_Config::createDefault();
-$config->set('HTML', 'DefinitionID', '1');
-$def = $config->getHTMLDefinition();
-$def->addAttribute('a', 'tabindex', 'Number');
-
-

- In the above example, the configuration is still at the defaults, but - using the advanced API, an extra attribute has been added. The - configuration object normally has no way of knowing that this change - has taken place, so it needs an extra directive: %HTML.DefinitionID. - If someone else attempts to use the default configuration, these two - pieces of code will not clobber each other in the cache, since one has - an extra directive attached to it. -

-

- You must specify a value to this directive to use the - advanced API features. -

---# vim: et sw=4 sts=4 +HTML.DefinitionID +TYPE: string/null +DEFAULT: NULL +VERSION: 2.0.0 +--DESCRIPTION-- + +

+ Unique identifier for a custom-built HTML definition. If you edit + the raw version of the HTMLDefinition, introducing changes that the + configuration object does not reflect, you must specify this variable. + If you change your custom edits, you should change this directive, or + clear your cache. Example: +

+
+$config = HTMLPurifier_Config::createDefault();
+$config->set('HTML', 'DefinitionID', '1');
+$def = $config->getHTMLDefinition();
+$def->addAttribute('a', 'tabindex', 'Number');
+
+

+ In the above example, the configuration is still at the defaults, but + using the advanced API, an extra attribute has been added. The + configuration object normally has no way of knowing that this change + has taken place, so it needs an extra directive: %HTML.DefinitionID. + If someone else attempts to use the default configuration, these two + pieces of code will not clobber each other in the cache, since one has + an extra directive attached to it. +

+

+ You must specify a value to this directive to use the + advanced API features. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt index 0bb5a718d5..229ae0267a 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt @@ -1,16 +1,16 @@ -HTML.DefinitionRev -TYPE: int -VERSION: 2.0.0 -DEFAULT: 1 ---DESCRIPTION-- - -

- Revision identifier for your custom definition specified in - %HTML.DefinitionID. This serves the same purpose: uniquely identifying - your custom definition, but this one does so in a chronological - context: revision 3 is more up-to-date then revision 2. Thus, when - this gets incremented, the cache handling is smart enough to clean - up any older revisions of your definition as well as flush the - cache. -

---# vim: et sw=4 sts=4 +HTML.DefinitionRev +TYPE: int +VERSION: 2.0.0 +DEFAULT: 1 +--DESCRIPTION-- + +

+ Revision identifier for your custom definition specified in + %HTML.DefinitionID. This serves the same purpose: uniquely identifying + your custom definition, but this one does so in a chronological + context: revision 3 is more up-to-date then revision 2. Thus, when + this gets incremented, the cache handling is smart enough to clean + up any older revisions of your definition as well as flush the + cache. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Doctype.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Doctype.txt index a6969b9957..9dab497f2f 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Doctype.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Doctype.txt @@ -1,11 +1,11 @@ -HTML.Doctype -TYPE: string/null -DEFAULT: NULL ---DESCRIPTION-- -Doctype to use during filtering. Technically speaking this is not actually -a doctype (as it does not identify a corresponding DTD), but we are using -this name for sake of simplicity. When non-blank, this will override any -older directives like %HTML.XHTML or %HTML.Strict. ---ALLOWED-- -'HTML 4.01 Transitional', 'HTML 4.01 Strict', 'XHTML 1.0 Transitional', 'XHTML 1.0 Strict', 'XHTML 1.1' ---# vim: et sw=4 sts=4 +HTML.Doctype +TYPE: string/null +DEFAULT: NULL +--DESCRIPTION-- +Doctype to use during filtering. Technically speaking this is not actually +a doctype (as it does not identify a corresponding DTD), but we are using +this name for sake of simplicity. When non-blank, this will override any +older directives like %HTML.XHTML or %HTML.Strict. +--ALLOWED-- +'HTML 4.01 Transitional', 'HTML 4.01 Strict', 'XHTML 1.0 Transitional', 'XHTML 1.0 Strict', 'XHTML 1.1' +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.FlashAllowFullScreen.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.FlashAllowFullScreen.txt index 08d641f954..7878dc0bf6 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.FlashAllowFullScreen.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.FlashAllowFullScreen.txt @@ -1,11 +1,11 @@ -HTML.FlashAllowFullScreen -TYPE: bool -VERSION: 4.2.0 -DEFAULT: false ---DESCRIPTION-- -

- Whether or not to permit embedded Flash content from - %HTML.SafeObject to expand to the full screen. Corresponds to - the allowFullScreen parameter. -

---# vim: et sw=4 sts=4 +HTML.FlashAllowFullScreen +TYPE: bool +VERSION: 4.2.0 +DEFAULT: false +--DESCRIPTION-- +

+ Whether or not to permit embedded Flash content from + %HTML.SafeObject to expand to the full screen. Corresponds to + the allowFullScreen parameter. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenAttributes.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenAttributes.txt index 2b8df97cbd..57358f9bad 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenAttributes.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenAttributes.txt @@ -1,21 +1,21 @@ -HTML.ForbiddenAttributes -TYPE: lookup -VERSION: 3.1.0 -DEFAULT: array() ---DESCRIPTION-- -

- While this directive is similar to %HTML.AllowedAttributes, for - forwards-compatibility with XML, this attribute has a different syntax. Instead of - tag.attr, use tag@attr. To disallow href - attributes in a tags, set this directive to - a@href. You can also disallow an attribute globally with - attr or *@attr (either syntax is fine; the latter - is provided for consistency with %HTML.AllowedAttributes). -

-

- Warning: This directive complements %HTML.ForbiddenElements, - accordingly, check - out that directive for a discussion of why you - should think twice before using this directive. -

---# vim: et sw=4 sts=4 +HTML.ForbiddenAttributes +TYPE: lookup +VERSION: 3.1.0 +DEFAULT: array() +--DESCRIPTION-- +

+ While this directive is similar to %HTML.AllowedAttributes, for + forwards-compatibility with XML, this attribute has a different syntax. Instead of + tag.attr, use tag@attr. To disallow href + attributes in a tags, set this directive to + a@href. You can also disallow an attribute globally with + attr or *@attr (either syntax is fine; the latter + is provided for consistency with %HTML.AllowedAttributes). +

+

+ Warning: This directive complements %HTML.ForbiddenElements, + accordingly, check + out that directive for a discussion of why you + should think twice before using this directive. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenElements.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenElements.txt index 40466c463d..93a53e14fb 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenElements.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenElements.txt @@ -1,20 +1,20 @@ -HTML.ForbiddenElements -TYPE: lookup -VERSION: 3.1.0 -DEFAULT: array() ---DESCRIPTION-- -

- This was, perhaps, the most requested feature ever in HTML - Purifier. Please don't abuse it! This is the logical inverse of - %HTML.AllowedElements, and it will override that directive, or any - other directive. -

-

- If possible, %HTML.Allowed is recommended over this directive, because it - can sometimes be difficult to tell whether or not you've forbidden all of - the behavior you would like to disallow. If you forbid img - with the expectation of preventing images on your site, you'll be in for - a nasty surprise when people start using the background-image - CSS property. -

---# vim: et sw=4 sts=4 +HTML.ForbiddenElements +TYPE: lookup +VERSION: 3.1.0 +DEFAULT: array() +--DESCRIPTION-- +

+ This was, perhaps, the most requested feature ever in HTML + Purifier. Please don't abuse it! This is the logical inverse of + %HTML.AllowedElements, and it will override that directive, or any + other directive. +

+

+ If possible, %HTML.Allowed is recommended over this directive, because it + can sometimes be difficult to tell whether or not you've forbidden all of + the behavior you would like to disallow. If you forbid img + with the expectation of preventing images on your site, you'll be in for + a nasty surprise when people start using the background-image + CSS property. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Forms.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Forms.txt new file mode 100644 index 0000000000..4a432d89b7 --- /dev/null +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Forms.txt @@ -0,0 +1,11 @@ +HTML.Forms +TYPE: bool +VERSION: 4.13.0 +DEFAULT: false +--DESCRIPTION-- +

+ Whether or not to permit form elements in the user input, regardless of + %HTML.Trusted value. Please be very careful when using this functionality, as + enabling forms in untrusted documents may allow for phishing attacks. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt index 3197479548..e424c386ec 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt @@ -1,14 +1,14 @@ -HTML.MaxImgLength -TYPE: int/null -DEFAULT: 1200 -VERSION: 3.1.1 ---DESCRIPTION-- -

- This directive controls the maximum number of pixels in the width and - height attributes in img tags. This is - in place to prevent imagecrash attacks, disable with null at your own risk. - This directive is similar to %CSS.MaxImgLength, and both should be - concurrently edited, although there are - subtle differences in the input format (the HTML max is an integer). -

---# vim: et sw=4 sts=4 +HTML.MaxImgLength +TYPE: int/null +DEFAULT: 1200 +VERSION: 3.1.1 +--DESCRIPTION-- +

+ This directive controls the maximum number of pixels in the width and + height attributes in img tags. This is + in place to prevent imagecrash attacks, disable with null at your own risk. + This directive is similar to %CSS.MaxImgLength, and both should be + concurrently edited, although there are + subtle differences in the input format (the HTML max is an integer). +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Nofollow.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Nofollow.txt index 7aa356353c..700b30924a 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Nofollow.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Nofollow.txt @@ -1,7 +1,7 @@ -HTML.Nofollow -TYPE: bool -VERSION: 4.3.0 -DEFAULT: FALSE ---DESCRIPTION-- -If enabled, nofollow rel attributes are added to all outgoing links. ---# vim: et sw=4 sts=4 +HTML.Nofollow +TYPE: bool +VERSION: 4.3.0 +DEFAULT: FALSE +--DESCRIPTION-- +If enabled, nofollow rel attributes are added to all outgoing links. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt index 2d2fbd1175..62e8e160c7 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt @@ -1,12 +1,12 @@ -HTML.Parent -TYPE: string -VERSION: 1.3.0 -DEFAULT: 'div' ---DESCRIPTION-- - -

- String name of element that HTML fragment passed to library will be - inserted in. An interesting variation would be using span as the - parent element, meaning that only inline tags would be allowed. -

---# vim: et sw=4 sts=4 +HTML.Parent +TYPE: string +VERSION: 1.3.0 +DEFAULT: 'div' +--DESCRIPTION-- + +

+ String name of element that HTML fragment passed to library will be + inserted in. An interesting variation would be using span as the + parent element, meaning that only inline tags would be allowed. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Proprietary.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Proprietary.txt index b3c45e1909..dfb720496d 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Proprietary.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Proprietary.txt @@ -1,12 +1,12 @@ -HTML.Proprietary -TYPE: bool -VERSION: 3.1.0 -DEFAULT: false ---DESCRIPTION-- -

- Whether or not to allow proprietary elements and attributes in your - documents, as per HTMLPurifier_HTMLModule_Proprietary. - Warning: This can cause your documents to stop - validating! -

---# vim: et sw=4 sts=4 +HTML.Proprietary +TYPE: bool +VERSION: 3.1.0 +DEFAULT: false +--DESCRIPTION-- +

+ Whether or not to allow proprietary elements and attributes in your + documents, as per HTMLPurifier_HTMLModule_Proprietary. + Warning: This can cause your documents to stop + validating! +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeEmbed.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeEmbed.txt index 556fa674f8..cdda09a4c5 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeEmbed.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeEmbed.txt @@ -1,13 +1,13 @@ -HTML.SafeEmbed -TYPE: bool -VERSION: 3.1.1 -DEFAULT: false ---DESCRIPTION-- -

- Whether or not to permit embed tags in documents, with a number of extra - security features added to prevent script execution. This is similar to - what websites like MySpace do to embed tags. Embed is a proprietary - element and will cause your website to stop validating; you should - see if you can use %Output.FlashCompat with %HTML.SafeObject instead - first.

---# vim: et sw=4 sts=4 +HTML.SafeEmbed +TYPE: bool +VERSION: 3.1.1 +DEFAULT: false +--DESCRIPTION-- +

+ Whether or not to permit embed tags in documents, with a number of extra + security features added to prevent script execution. This is similar to + what websites like MySpace do to embed tags. Embed is a proprietary + element and will cause your website to stop validating; you should + see if you can use %Output.FlashCompat with %HTML.SafeObject instead + first.

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeIframe.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeIframe.txt index 295a8cf660..5eb6ec2b5a 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeIframe.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeIframe.txt @@ -1,13 +1,13 @@ -HTML.SafeIframe -TYPE: bool -VERSION: 4.4.0 -DEFAULT: false ---DESCRIPTION-- -

- Whether or not to permit iframe tags in untrusted documents. This - directive must be accompanied by a whitelist of permitted iframes, - such as %URI.SafeIframeRegexp, otherwise it will fatally error. - This directive has no effect on strict doctypes, as iframes are not - valid. -

---# vim: et sw=4 sts=4 +HTML.SafeIframe +TYPE: bool +VERSION: 4.4.0 +DEFAULT: false +--DESCRIPTION-- +

+ Whether or not to permit iframe tags in untrusted documents. This + directive must be accompanied by a whitelist of permitted iframes, + such as %URI.SafeIframeRegexp, otherwise it will fatally error. + This directive has no effect on strict doctypes, as iframes are not + valid. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeObject.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeObject.txt index 07f6e536e3..ceb342e22b 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeObject.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeObject.txt @@ -1,13 +1,13 @@ -HTML.SafeObject -TYPE: bool -VERSION: 3.1.1 -DEFAULT: false ---DESCRIPTION-- -

- Whether or not to permit object tags in documents, with a number of extra - security features added to prevent script execution. This is similar to - what websites like MySpace do to object tags. You should also enable - %Output.FlashCompat in order to generate Internet Explorer - compatibility code for your object tags. -

---# vim: et sw=4 sts=4 +HTML.SafeObject +TYPE: bool +VERSION: 3.1.1 +DEFAULT: false +--DESCRIPTION-- +

+ Whether or not to permit object tags in documents, with a number of extra + security features added to prevent script execution. This is similar to + what websites like MySpace do to object tags. You should also enable + %Output.FlashCompat in order to generate Internet Explorer + compatibility code for your object tags. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeScripting.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeScripting.txt index 641b4a8d6b..5ebc7a19d5 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeScripting.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeScripting.txt @@ -1,10 +1,10 @@ -HTML.SafeScripting -TYPE: lookup -VERSION: 4.5.0 -DEFAULT: array() ---DESCRIPTION-- -

- Whether or not to permit script tags to external scripts in documents. - Inline scripting is not allowed, and the script must match an explicit whitelist. -

---# vim: et sw=4 sts=4 +HTML.SafeScripting +TYPE: lookup +VERSION: 4.5.0 +DEFAULT: array() +--DESCRIPTION-- +

+ Whether or not to permit script tags to external scripts in documents. + Inline scripting is not allowed, and the script must match an explicit whitelist. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Strict.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Strict.txt index d99663a5ee..a8b1de56be 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Strict.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Strict.txt @@ -1,9 +1,9 @@ -HTML.Strict -TYPE: bool -VERSION: 1.3.0 -DEFAULT: false -DEPRECATED-VERSION: 1.7.0 -DEPRECATED-USE: HTML.Doctype ---DESCRIPTION-- -Determines whether or not to use Transitional (loose) or Strict rulesets. ---# vim: et sw=4 sts=4 +HTML.Strict +TYPE: bool +VERSION: 1.3.0 +DEFAULT: false +DEPRECATED-VERSION: 1.7.0 +DEPRECATED-USE: HTML.Doctype +--DESCRIPTION-- +Determines whether or not to use Transitional (loose) or Strict rulesets. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.TargetBlank.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.TargetBlank.txt index d65f0d041a..587a16778b 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.TargetBlank.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.TargetBlank.txt @@ -1,8 +1,8 @@ -HTML.TargetBlank -TYPE: bool -VERSION: 4.4.0 -DEFAULT: FALSE ---DESCRIPTION-- -If enabled, target=blank attributes are added to all outgoing links. -(This includes links from an HTTPS version of a page to an HTTP version.) ---# vim: et sw=4 sts=4 +HTML.TargetBlank +TYPE: bool +VERSION: 4.4.0 +DEFAULT: FALSE +--DESCRIPTION-- +If enabled, target=blank attributes are added to all outgoing links. +(This includes links from an HTTPS version of a page to an HTTP version.) +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoopener.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoopener.txt index 05cb3424f8..dd514c0def 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoopener.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoopener.txt @@ -1,10 +1,10 @@ ---# vim: et sw=4 sts=4 -HTML.TargetNoopener -TYPE: bool -VERSION: 4.8.0 -DEFAULT: TRUE ---DESCRIPTION-- -If enabled, noopener rel attributes are added to links which have -a target attribute associated with them. This prevents malicious -destinations from overwriting the original window. ---# vim: et sw=4 sts=4 +--# vim: et sw=4 sts=4 +HTML.TargetNoopener +TYPE: bool +VERSION: 4.8.0 +DEFAULT: TRUE +--DESCRIPTION-- +If enabled, noopener rel attributes are added to links which have +a target attribute associated with them. This prevents malicious +destinations from overwriting the original window. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoreferrer.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoreferrer.txt index 993a81704e..cb5a0b0e5e 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoreferrer.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoreferrer.txt @@ -1,9 +1,9 @@ -HTML.TargetNoreferrer -TYPE: bool -VERSION: 4.8.0 -DEFAULT: TRUE ---DESCRIPTION-- -If enabled, noreferrer rel attributes are added to links which have -a target attribute associated with them. This prevents malicious -destinations from overwriting the original window. ---# vim: et sw=4 sts=4 +HTML.TargetNoreferrer +TYPE: bool +VERSION: 4.8.0 +DEFAULT: TRUE +--DESCRIPTION-- +If enabled, noreferrer rel attributes are added to links which have +a target attribute associated with them. This prevents malicious +destinations from overwriting the original window. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyAdd.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyAdd.txt index 602453f6ee..b4c271b7fa 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyAdd.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyAdd.txt @@ -1,8 +1,8 @@ -HTML.TidyAdd -TYPE: lookup -VERSION: 2.0.0 -DEFAULT: array() ---DESCRIPTION-- - -Fixes to add to the default set of Tidy fixes as per your level. ---# vim: et sw=4 sts=4 +HTML.TidyAdd +TYPE: lookup +VERSION: 2.0.0 +DEFAULT: array() +--DESCRIPTION-- + +Fixes to add to the default set of Tidy fixes as per your level. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt index bf943e8f01..4186ccd0d1 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt @@ -1,24 +1,24 @@ -HTML.TidyLevel -TYPE: string -VERSION: 2.0.0 -DEFAULT: 'medium' ---DESCRIPTION-- - -

General level of cleanliness the Tidy module should enforce. -There are four allowed values:

-
-
none
-
No extra tidying should be done
-
light
-
Only fix elements that would be discarded otherwise due to - lack of support in doctype
-
medium
-
Enforce best practices
-
heavy
-
Transform all deprecated elements and attributes to standards - compliant equivalents
-
- ---ALLOWED-- -'none', 'light', 'medium', 'heavy' ---# vim: et sw=4 sts=4 +HTML.TidyLevel +TYPE: string +VERSION: 2.0.0 +DEFAULT: 'medium' +--DESCRIPTION-- + +

General level of cleanliness the Tidy module should enforce. +There are four allowed values:

+
+
none
+
No extra tidying should be done
+
light
+
Only fix elements that would be discarded otherwise due to + lack of support in doctype
+
medium
+
Enforce best practices
+
heavy
+
Transform all deprecated elements and attributes to standards + compliant equivalents
+
+ +--ALLOWED-- +'none', 'light', 'medium', 'heavy' +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyRemove.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyRemove.txt index 92cca2a434..996762bd1d 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyRemove.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyRemove.txt @@ -1,8 +1,8 @@ -HTML.TidyRemove -TYPE: lookup -VERSION: 2.0.0 -DEFAULT: array() ---DESCRIPTION-- - -Fixes to remove from the default set of Tidy fixes as per your level. ---# vim: et sw=4 sts=4 +HTML.TidyRemove +TYPE: lookup +VERSION: 2.0.0 +DEFAULT: array() +--DESCRIPTION-- + +Fixes to remove from the default set of Tidy fixes as per your level. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt index bc8e65499d..1db9237e9e 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt @@ -1,9 +1,9 @@ -HTML.Trusted -TYPE: bool -VERSION: 2.0.0 -DEFAULT: false ---DESCRIPTION-- -Indicates whether or not the user input is trusted or not. If the input is -trusted, a more expansive set of allowed tags and attributes will be used. -See also %CSS.Trusted. ---# vim: et sw=4 sts=4 +HTML.Trusted +TYPE: bool +VERSION: 2.0.0 +DEFAULT: false +--DESCRIPTION-- +Indicates whether or not the user input is trusted or not. If the input is +trusted, a more expansive set of allowed tags and attributes will be used. +See also %CSS.Trusted. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.XHTML.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.XHTML.txt index a3c2f42c3a..2a47e384f4 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.XHTML.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/HTML.XHTML.txt @@ -1,11 +1,11 @@ -HTML.XHTML -TYPE: bool -DEFAULT: true -VERSION: 1.1.0 -DEPRECATED-VERSION: 1.7.0 -DEPRECATED-USE: HTML.Doctype ---DESCRIPTION-- -Determines whether or not output is XHTML 1.0 or HTML 4.01 flavor. ---ALIASES-- -Core.XHTML ---# vim: et sw=4 sts=4 +HTML.XHTML +TYPE: bool +DEFAULT: true +VERSION: 1.1.0 +DEPRECATED-VERSION: 1.7.0 +DEPRECATED-USE: HTML.Doctype +--DESCRIPTION-- +Determines whether or not output is XHTML 1.0 or HTML 4.01 flavor. +--ALIASES-- +Core.XHTML +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Output.CommentScriptContents.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Output.CommentScriptContents.txt index 2a13704700..08921fde70 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Output.CommentScriptContents.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Output.CommentScriptContents.txt @@ -1,10 +1,10 @@ -Output.CommentScriptContents -TYPE: bool -VERSION: 2.0.0 -DEFAULT: true ---DESCRIPTION-- -Determines whether or not HTML Purifier should attempt to fix up the -contents of script tags for legacy browsers with comments. ---ALIASES-- -Core.CommentScriptContents ---# vim: et sw=4 sts=4 +Output.CommentScriptContents +TYPE: bool +VERSION: 2.0.0 +DEFAULT: true +--DESCRIPTION-- +Determines whether or not HTML Purifier should attempt to fix up the +contents of script tags for legacy browsers with comments. +--ALIASES-- +Core.CommentScriptContents +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Output.FixInnerHTML.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Output.FixInnerHTML.txt index d215ba2d30..d6f0d9f295 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Output.FixInnerHTML.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Output.FixInnerHTML.txt @@ -1,15 +1,15 @@ -Output.FixInnerHTML -TYPE: bool -VERSION: 4.3.0 -DEFAULT: true ---DESCRIPTION-- -

- If true, HTML Purifier will protect against Internet Explorer's - mishandling of the innerHTML attribute by appending - a space to any attribute that does not contain angled brackets, spaces - or quotes, but contains a backtick. This slightly changes the - semantics of any given attribute, so if this is unacceptable and - you do not use innerHTML on any of your pages, you can - turn this directive off. -

---# vim: et sw=4 sts=4 +Output.FixInnerHTML +TYPE: bool +VERSION: 4.3.0 +DEFAULT: true +--DESCRIPTION-- +

+ If true, HTML Purifier will protect against Internet Explorer's + mishandling of the innerHTML attribute by appending + a space to any attribute that does not contain angled brackets, spaces + or quotes, but contains a backtick. This slightly changes the + semantics of any given attribute, so if this is unacceptable and + you do not use innerHTML on any of your pages, you can + turn this directive off. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Output.FlashCompat.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Output.FlashCompat.txt index e58f91aa8c..93398e8598 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Output.FlashCompat.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Output.FlashCompat.txt @@ -1,11 +1,11 @@ -Output.FlashCompat -TYPE: bool -VERSION: 4.1.0 -DEFAULT: false ---DESCRIPTION-- -

- If true, HTML Purifier will generate Internet Explorer compatibility - code for all object code. This is highly recommended if you enable - %HTML.SafeObject. -

---# vim: et sw=4 sts=4 +Output.FlashCompat +TYPE: bool +VERSION: 4.1.0 +DEFAULT: false +--DESCRIPTION-- +

+ If true, HTML Purifier will generate Internet Explorer compatibility + code for all object code. This is highly recommended if you enable + %HTML.SafeObject. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Output.Newline.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Output.Newline.txt index 4bb9025231..79f8ad82cf 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Output.Newline.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Output.Newline.txt @@ -1,13 +1,13 @@ -Output.Newline -TYPE: string/null -VERSION: 2.0.1 -DEFAULT: NULL ---DESCRIPTION-- - -

- Newline string to format final output with. If left null, HTML Purifier - will auto-detect the default newline type of the system and use that; - you can manually override it here. Remember, \r\n is Windows, \r - is Mac, and \n is Unix. -

---# vim: et sw=4 sts=4 +Output.Newline +TYPE: string/null +VERSION: 2.0.1 +DEFAULT: NULL +--DESCRIPTION-- + +

+ Newline string to format final output with. If left null, HTML Purifier + will auto-detect the default newline type of the system and use that; + you can manually override it here. Remember, \r\n is Windows, \r + is Mac, and \n is Unix. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Output.SortAttr.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Output.SortAttr.txt index 3223106517..232b02362a 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Output.SortAttr.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Output.SortAttr.txt @@ -1,14 +1,14 @@ -Output.SortAttr -TYPE: bool -VERSION: 3.2.0 -DEFAULT: false ---DESCRIPTION-- -

- If true, HTML Purifier will sort attributes by name before writing them back - to the document, converting a tag like: <el b="" a="" c="" /> - to <el a="" b="" c="" />. This is a workaround for - a bug in FCKeditor which causes it to swap attributes order, adding noise - to text diffs. If you're not seeing this bug, chances are, you don't need - this directive. -

---# vim: et sw=4 sts=4 +Output.SortAttr +TYPE: bool +VERSION: 3.2.0 +DEFAULT: false +--DESCRIPTION-- +

+ If true, HTML Purifier will sort attributes by name before writing them back + to the document, converting a tag like: <el b="" a="" c="" /> + to <el a="" b="" c="" />. This is a workaround for + a bug in FCKeditor which causes it to swap attributes order, adding noise + to text diffs. If you're not seeing this bug, chances are, you don't need + this directive. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Output.TidyFormat.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Output.TidyFormat.txt index 23dd4d3d5d..06bab00a0a 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Output.TidyFormat.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Output.TidyFormat.txt @@ -1,25 +1,25 @@ -Output.TidyFormat -TYPE: bool -VERSION: 1.1.1 -DEFAULT: false ---DESCRIPTION-- -

- Determines whether or not to run Tidy on the final output for pretty - formatting reasons, such as indentation and wrap. -

-

- This can greatly improve readability for editors who are hand-editing - the HTML, but is by no means necessary as HTML Purifier has already - fixed all major errors the HTML may have had. Tidy is a non-default - extension, and this directive will silently fail if Tidy is not - available. -

-

- If you are looking to make the overall look of your page's source - better, I recommend running Tidy on the entire page rather than just - user-content (after all, the indentation relative to the containing - blocks will be incorrect). -

---ALIASES-- -Core.TidyFormat ---# vim: et sw=4 sts=4 +Output.TidyFormat +TYPE: bool +VERSION: 1.1.1 +DEFAULT: false +--DESCRIPTION-- +

+ Determines whether or not to run Tidy on the final output for pretty + formatting reasons, such as indentation and wrap. +

+

+ This can greatly improve readability for editors who are hand-editing + the HTML, but is by no means necessary as HTML Purifier has already + fixed all major errors the HTML may have had. Tidy is a non-default + extension, and this directive will silently fail if Tidy is not + available. +

+

+ If you are looking to make the overall look of your page's source + better, I recommend running Tidy on the entire page rather than just + user-content (after all, the indentation relative to the containing + blocks will be incorrect). +

+--ALIASES-- +Core.TidyFormat +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Test.ForceNoIconv.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Test.ForceNoIconv.txt index d1820cdbd5..071bc0295d 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/Test.ForceNoIconv.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/Test.ForceNoIconv.txt @@ -1,7 +1,7 @@ -Test.ForceNoIconv -TYPE: bool -DEFAULT: false ---DESCRIPTION-- -When set to true, HTMLPurifier_Encoder will act as if iconv does not exist -and use only pure PHP implementations. ---# vim: et sw=4 sts=4 +Test.ForceNoIconv +TYPE: bool +DEFAULT: false +--DESCRIPTION-- +When set to true, HTMLPurifier_Encoder will act as if iconv does not exist +and use only pure PHP implementations. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt index 0b0533a77f..eb97307e20 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt @@ -1,18 +1,18 @@ -URI.AllowedSchemes -TYPE: lookup ---DEFAULT-- -array ( - 'http' => true, - 'https' => true, - 'mailto' => true, - 'ftp' => true, - 'nntp' => true, - 'news' => true, - 'tel' => true, -) ---DESCRIPTION-- -Whitelist that defines the schemes that a URI is allowed to have. This -prevents XSS attacks from using pseudo-schemes like javascript or mocha. -There is also support for the data and file -URI schemes, but they are not enabled by default. ---# vim: et sw=4 sts=4 +URI.AllowedSchemes +TYPE: lookup +--DEFAULT-- +array ( + 'http' => true, + 'https' => true, + 'mailto' => true, + 'ftp' => true, + 'nntp' => true, + 'news' => true, + 'tel' => true, +) +--DESCRIPTION-- +Whitelist that defines the schemes that a URI is allowed to have. This +prevents XSS attacks from using pseudo-schemes like javascript or mocha. +There is also support for the data and file +URI schemes, but they are not enabled by default. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.Base.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.Base.txt index ba4730808f..876f0680cf 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.Base.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.Base.txt @@ -1,17 +1,17 @@ -URI.Base -TYPE: string/null -VERSION: 2.1.0 -DEFAULT: NULL ---DESCRIPTION-- - -

- The base URI is the URI of the document this purified HTML will be - inserted into. This information is important if HTML Purifier needs - to calculate absolute URIs from relative URIs, such as when %URI.MakeAbsolute - is on. You may use a non-absolute URI for this value, but behavior - may vary (%URI.MakeAbsolute deals nicely with both absolute and - relative paths, but forwards-compatibility is not guaranteed). - Warning: If set, the scheme on this URI - overrides the one specified by %URI.DefaultScheme. -

---# vim: et sw=4 sts=4 +URI.Base +TYPE: string/null +VERSION: 2.1.0 +DEFAULT: NULL +--DESCRIPTION-- + +

+ The base URI is the URI of the document this purified HTML will be + inserted into. This information is important if HTML Purifier needs + to calculate absolute URIs from relative URIs, such as when %URI.MakeAbsolute + is on. You may use a non-absolute URI for this value, but behavior + may vary (%URI.MakeAbsolute deals nicely with both absolute and + relative paths, but forwards-compatibility is not guaranteed). + Warning: If set, the scheme on this URI + overrides the one specified by %URI.DefaultScheme. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt index 981e443251..834bc08c0b 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt @@ -1,15 +1,15 @@ -URI.DefaultScheme -TYPE: string/null -DEFAULT: 'http' ---DESCRIPTION-- - -

- Defines through what scheme the output will be served, in order to - select the proper object validator when no scheme information is present. -

- -

- Starting with HTML Purifier 4.9.0, the default scheme can be null, in - which case we reject all URIs which do not have explicit schemes. -

---# vim: et sw=4 sts=4 +URI.DefaultScheme +TYPE: string/null +DEFAULT: 'http' +--DESCRIPTION-- + +

+ Defines through what scheme the output will be served, in order to + select the proper object validator when no scheme information is present. +

+ +

+ Starting with HTML Purifier 4.9.0, the default scheme can be null, in + which case we reject all URIs which do not have explicit schemes. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.DefinitionID.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.DefinitionID.txt index 523204c08a..f05312ba86 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.DefinitionID.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.DefinitionID.txt @@ -1,11 +1,11 @@ -URI.DefinitionID -TYPE: string/null -VERSION: 2.1.0 -DEFAULT: NULL ---DESCRIPTION-- - -

- Unique identifier for a custom-built URI definition. If you want - to add custom URIFilters, you must specify this value. -

---# vim: et sw=4 sts=4 +URI.DefinitionID +TYPE: string/null +VERSION: 2.1.0 +DEFAULT: NULL +--DESCRIPTION-- + +

+ Unique identifier for a custom-built URI definition. If you want + to add custom URIFilters, you must specify this value. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.DefinitionRev.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.DefinitionRev.txt index a9c07b1a39..80cfea93f7 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.DefinitionRev.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.DefinitionRev.txt @@ -1,11 +1,11 @@ -URI.DefinitionRev -TYPE: int -VERSION: 2.1.0 -DEFAULT: 1 ---DESCRIPTION-- - -

- Revision identifier for your custom definition. See - %HTML.DefinitionRev for details. -

---# vim: et sw=4 sts=4 +URI.DefinitionRev +TYPE: int +VERSION: 2.1.0 +DEFAULT: 1 +--DESCRIPTION-- + +

+ Revision identifier for your custom definition. See + %HTML.DefinitionRev for details. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.Disable.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.Disable.txt index b19ca1d5b8..71ce025a2d 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.Disable.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.Disable.txt @@ -1,14 +1,14 @@ -URI.Disable -TYPE: bool -VERSION: 1.3.0 -DEFAULT: false ---DESCRIPTION-- - -

- Disables all URIs in all forms. Not sure why you'd want to do that - (after all, the Internet's founded on the notion of a hyperlink). -

- ---ALIASES-- -Attr.DisableURI ---# vim: et sw=4 sts=4 +URI.Disable +TYPE: bool +VERSION: 1.3.0 +DEFAULT: false +--DESCRIPTION-- + +

+ Disables all URIs in all forms. Not sure why you'd want to do that + (after all, the Internet's founded on the notion of a hyperlink). +

+ +--ALIASES-- +Attr.DisableURI +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.DisableExternal.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.DisableExternal.txt index 9132ea4f5f..13c122c8ce 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.DisableExternal.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.DisableExternal.txt @@ -1,11 +1,11 @@ -URI.DisableExternal -TYPE: bool -VERSION: 1.2.0 -DEFAULT: false ---DESCRIPTION-- -Disables links to external websites. This is a highly effective anti-spam -and anti-pagerank-leech measure, but comes at a hefty price: nolinks or -images outside of your domain will be allowed. Non-linkified URIs will -still be preserved. If you want to be able to link to subdomains or use -absolute URIs, specify %URI.Host for your website. ---# vim: et sw=4 sts=4 +URI.DisableExternal +TYPE: bool +VERSION: 1.2.0 +DEFAULT: false +--DESCRIPTION-- +Disables links to external websites. This is a highly effective anti-spam +and anti-pagerank-leech measure, but comes at a hefty price: nolinks or +images outside of your domain will be allowed. Non-linkified URIs will +still be preserved. If you want to be able to link to subdomains or use +absolute URIs, specify %URI.Host for your website. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.DisableExternalResources.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.DisableExternalResources.txt index d74bc1e3da..abcc1efd61 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.DisableExternalResources.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.DisableExternalResources.txt @@ -1,13 +1,13 @@ -URI.DisableExternalResources -TYPE: bool -VERSION: 1.3.0 -DEFAULT: false ---DESCRIPTION-- -Disables the embedding of external resources, preventing users from -embedding things like images from other hosts. This prevents access -tracking (good for email viewers), bandwidth leeching, cross-site request -forging, goatse.cx posting, and other nasties, but also results in a loss -of end-user functionality (they can't directly post a pic they posted from -Flickr anymore). Use it if you don't have a robust user-content moderation -team. ---# vim: et sw=4 sts=4 +URI.DisableExternalResources +TYPE: bool +VERSION: 1.3.0 +DEFAULT: false +--DESCRIPTION-- +Disables the embedding of external resources, preventing users from +embedding things like images from other hosts. This prevents access +tracking (good for email viewers), bandwidth leeching, cross-site request +forging, goatse.cx posting, and other nasties, but also results in a loss +of end-user functionality (they can't directly post a pic they posted from +Flickr anymore). Use it if you don't have a robust user-content moderation +team. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt index 6c106144a0..f891de4996 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt @@ -1,15 +1,15 @@ -URI.DisableResources -TYPE: bool -VERSION: 4.2.0 -DEFAULT: false ---DESCRIPTION-- -

- Disables embedding resources, essentially meaning no pictures. You can - still link to them though. See %URI.DisableExternalResources for why - this might be a good idea. -

-

- Note: While this directive has been available since 1.3.0, - it didn't actually start doing anything until 4.2.0. -

---# vim: et sw=4 sts=4 +URI.DisableResources +TYPE: bool +VERSION: 4.2.0 +DEFAULT: false +--DESCRIPTION-- +

+ Disables embedding resources, essentially meaning no pictures. You can + still link to them though. See %URI.DisableExternalResources for why + this might be a good idea. +

+

+ Note: While this directive has been available since 1.3.0, + it didn't actually start doing anything until 4.2.0. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.Host.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.Host.txt index ba0e6bce19..ee83b121de 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.Host.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.Host.txt @@ -1,19 +1,19 @@ -URI.Host -TYPE: string/null -VERSION: 1.2.0 -DEFAULT: NULL ---DESCRIPTION-- - -

- Defines the domain name of the server, so we can determine whether or - an absolute URI is from your website or not. Not strictly necessary, - as users should be using relative URIs to reference resources on your - website. It will, however, let you use absolute URIs to link to - subdomains of the domain you post here: i.e. example.com will allow - sub.example.com. However, higher up domains will still be excluded: - if you set %URI.Host to sub.example.com, example.com will be blocked. - Note: This directive overrides %URI.Base because - a given page may be on a sub-domain, but you wish HTML Purifier to be - more relaxed and allow some of the parent domains too. -

---# vim: et sw=4 sts=4 +URI.Host +TYPE: string/null +VERSION: 1.2.0 +DEFAULT: NULL +--DESCRIPTION-- + +

+ Defines the domain name of the server, so we can determine whether or + an absolute URI is from your website or not. Not strictly necessary, + as users should be using relative URIs to reference resources on your + website. It will, however, let you use absolute URIs to link to + subdomains of the domain you post here: i.e. example.com will allow + sub.example.com. However, higher up domains will still be excluded: + if you set %URI.Host to sub.example.com, example.com will be blocked. + Note: This directive overrides %URI.Base because + a given page may be on a sub-domain, but you wish HTML Purifier to be + more relaxed and allow some of the parent domains too. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.HostBlacklist.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.HostBlacklist.txt index 825fef2769..0b6df7625d 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.HostBlacklist.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.HostBlacklist.txt @@ -1,9 +1,9 @@ -URI.HostBlacklist -TYPE: list -VERSION: 1.3.0 -DEFAULT: array() ---DESCRIPTION-- -List of strings that are forbidden in the host of any URI. Use it to kill -domain names of spam, etc. Note that it will catch anything in the domain, -so moo.com will catch moo.com.example.com. ---# vim: et sw=4 sts=4 +URI.HostBlacklist +TYPE: list +VERSION: 1.3.0 +DEFAULT: array() +--DESCRIPTION-- +List of strings that are forbidden in the host of any URI. Use it to kill +domain names of spam, etc. Note that it will catch anything in the domain, +so moo.com will catch moo.com.example.com. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt index eb58c7f1a4..4214900a59 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt @@ -1,13 +1,13 @@ -URI.MakeAbsolute -TYPE: bool -VERSION: 2.1.0 -DEFAULT: false ---DESCRIPTION-- - -

- Converts all URIs into absolute forms. This is useful when the HTML - being filtered assumes a specific base path, but will actually be - viewed in a different context (and setting an alternate base URI is - not possible). %URI.Base must be set for this directive to work. -

---# vim: et sw=4 sts=4 +URI.MakeAbsolute +TYPE: bool +VERSION: 2.1.0 +DEFAULT: false +--DESCRIPTION-- + +

+ Converts all URIs into absolute forms. This is useful when the HTML + being filtered assumes a specific base path, but will actually be + viewed in a different context (and setting an alternate base URI is + not possible). %URI.Base must be set for this directive to work. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt index bedd610d66..58c81dcc44 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt @@ -1,83 +1,83 @@ -URI.Munge -TYPE: string/null -VERSION: 1.3.0 -DEFAULT: NULL ---DESCRIPTION-- - -

- Munges all browsable (usually http, https and ftp) - absolute URIs into another URI, usually a URI redirection service. - This directive accepts a URI, formatted with a %s where - the url-encoded original URI should be inserted (sample: - http://www.google.com/url?q=%s). -

-

- Uses for this directive: -

-
    -
  • - Prevent PageRank leaks, while being fairly transparent - to users (you may also want to add some client side JavaScript to - override the text in the statusbar). Notice: - Many security experts believe that this form of protection does not deter spam-bots. -
  • -
  • - Redirect users to a splash page telling them they are leaving your - website. While this is poor usability practice, it is often mandated - in corporate environments. -
  • -
-

- Prior to HTML Purifier 3.1.1, this directive also enabled the munging - of browsable external resources, which could break things if your redirection - script was a splash page or used meta tags. To revert to - previous behavior, please use %URI.MungeResources. -

-

- You may want to also use %URI.MungeSecretKey along with this directive - in order to enforce what URIs your redirector script allows. Open - redirector scripts can be a security risk and negatively affect the - reputation of your domain name. -

-

- Starting with HTML Purifier 3.1.1, there is also these substitutions: -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
KeyDescriptionExample <a href="">
%r1 - The URI embeds a resource
(blank) - The URI is merely a link
%nThe name of the tag this URI came froma
%mThe name of the attribute this URI came fromhref
%pThe name of the CSS property this URI came from, or blank if irrelevant
-

- Admittedly, these letters are somewhat arbitrary; the only stipulation - was that they couldn't be a through f. r is for resource (I would have preferred - e, but you take what you can get), n is for name, m - was picked because it came after n (and I couldn't use a), p is for - property. -

---# vim: et sw=4 sts=4 +URI.Munge +TYPE: string/null +VERSION: 1.3.0 +DEFAULT: NULL +--DESCRIPTION-- + +

+ Munges all browsable (usually http, https and ftp) + absolute URIs into another URI, usually a URI redirection service. + This directive accepts a URI, formatted with a %s where + the url-encoded original URI should be inserted (sample: + http://www.google.com/url?q=%s). +

+

+ Uses for this directive: +

+
    +
  • + Prevent PageRank leaks, while being fairly transparent + to users (you may also want to add some client side JavaScript to + override the text in the statusbar). Notice: + Many security experts believe that this form of protection does not deter spam-bots. +
  • +
  • + Redirect users to a splash page telling them they are leaving your + website. While this is poor usability practice, it is often mandated + in corporate environments. +
  • +
+

+ Prior to HTML Purifier 3.1.1, this directive also enabled the munging + of browsable external resources, which could break things if your redirection + script was a splash page or used meta tags. To revert to + previous behavior, please use %URI.MungeResources. +

+

+ You may want to also use %URI.MungeSecretKey along with this directive + in order to enforce what URIs your redirector script allows. Open + redirector scripts can be a security risk and negatively affect the + reputation of your domain name. +

+

+ Starting with HTML Purifier 3.1.1, there is also these substitutions: +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyDescriptionExample <a href="">
%r1 - The URI embeds a resource
(blank) - The URI is merely a link
%nThe name of the tag this URI came froma
%mThe name of the attribute this URI came fromhref
%pThe name of the CSS property this URI came from, or blank if irrelevant
+

+ Admittedly, these letters are somewhat arbitrary; the only stipulation + was that they couldn't be a through f. r is for resource (I would have preferred + e, but you take what you can get), n is for name, m + was picked because it came after n (and I couldn't use a), p is for + property. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.MungeResources.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.MungeResources.txt index ed4b5b0d02..6fce0fdc37 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.MungeResources.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.MungeResources.txt @@ -1,17 +1,17 @@ -URI.MungeResources -TYPE: bool -VERSION: 3.1.1 -DEFAULT: false ---DESCRIPTION-- -

- If true, any URI munging directives like %URI.Munge - will also apply to embedded resources, such as <img src="">. - Be careful enabling this directive if you have a redirector script - that does not use the Location HTTP header; all of your images - and other embedded resources will break. -

-

- Warning: It is strongly advised you use this in conjunction - %URI.MungeSecretKey to mitigate the security risk of an open redirector. -

---# vim: et sw=4 sts=4 +URI.MungeResources +TYPE: bool +VERSION: 3.1.1 +DEFAULT: false +--DESCRIPTION-- +

+ If true, any URI munging directives like %URI.Munge + will also apply to embedded resources, such as <img src="">. + Be careful enabling this directive if you have a redirector script + that does not use the Location HTTP header; all of your images + and other embedded resources will break. +

+

+ Warning: It is strongly advised you use this in conjunction + %URI.MungeSecretKey to mitigate the security risk of an open redirector. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.MungeSecretKey.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.MungeSecretKey.txt index 123b6e26b8..1e17c1d461 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.MungeSecretKey.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.MungeSecretKey.txt @@ -1,30 +1,30 @@ -URI.MungeSecretKey -TYPE: string/null -VERSION: 3.1.1 -DEFAULT: NULL ---DESCRIPTION-- -

- This directive enables secure checksum generation along with %URI.Munge. - It should be set to a secure key that is not shared with anyone else. - The checksum can be placed in the URI using %t. Use of this checksum - affords an additional level of protection by allowing a redirector - to check if a URI has passed through HTML Purifier with this line: -

- -
$checksum === hash_hmac("sha256", $url, $secret_key)
- -

- If the output is TRUE, the redirector script should accept the URI. -

- -

- Please note that it would still be possible for an attacker to procure - secure hashes en-mass by abusing your website's Preview feature or the - like, but this service affords an additional level of protection - that should be combined with website blacklisting. -

- -

- Remember this has no effect if %URI.Munge is not on. -

---# vim: et sw=4 sts=4 +URI.MungeSecretKey +TYPE: string/null +VERSION: 3.1.1 +DEFAULT: NULL +--DESCRIPTION-- +

+ This directive enables secure checksum generation along with %URI.Munge. + It should be set to a secure key that is not shared with anyone else. + The checksum can be placed in the URI using %t. Use of this checksum + affords an additional level of protection by allowing a redirector + to check if a URI has passed through HTML Purifier with this line: +

+ +
$checksum === hash_hmac("sha256", $url, $secret_key)
+ +

+ If the output is TRUE, the redirector script should accept the URI. +

+ +

+ Please note that it would still be possible for an attacker to procure + secure hashes en-mass by abusing your website's Preview feature or the + like, but this service affords an additional level of protection + that should be combined with website blacklisting. +

+ +

+ Remember this has no effect if %URI.Munge is not on. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.OverrideAllowedSchemes.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.OverrideAllowedSchemes.txt index 8b387dea31..23331a4e79 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.OverrideAllowedSchemes.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.OverrideAllowedSchemes.txt @@ -1,9 +1,9 @@ -URI.OverrideAllowedSchemes -TYPE: bool -DEFAULT: true ---DESCRIPTION-- -If this is set to true (which it is by default), you can override -%URI.AllowedSchemes by simply registering a HTMLPurifier_URIScheme to the -registry. If false, you will also have to update that directive in order -to add more schemes. ---# vim: et sw=4 sts=4 +URI.OverrideAllowedSchemes +TYPE: bool +DEFAULT: true +--DESCRIPTION-- +If this is set to true (which it is by default), you can override +%URI.AllowedSchemes by simply registering a HTMLPurifier_URIScheme to the +registry. If false, you will also have to update that directive in order +to add more schemes. +--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.SafeIframeRegexp.txt b/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.SafeIframeRegexp.txt index 7e1f227f79..79084832be 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.SafeIframeRegexp.txt +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/URI.SafeIframeRegexp.txt @@ -1,22 +1,22 @@ -URI.SafeIframeRegexp -TYPE: string/null -VERSION: 4.4.0 -DEFAULT: NULL ---DESCRIPTION-- -

- A PCRE regular expression that will be matched against an iframe URI. This is - a relatively inflexible scheme, but works well enough for the most common - use-case of iframes: embedded video. This directive only has an effect if - %HTML.SafeIframe is enabled. Here are some example values: -

-
    -
  • %^http://www.youtube.com/embed/% - Allow YouTube videos
  • -
  • %^http://player.vimeo.com/video/% - Allow Vimeo videos
  • -
  • %^http://(www.youtube.com/embed/|player.vimeo.com/video/)% - Allow both
  • -
-

- Note that this directive does not give you enough granularity to, say, disable - all autoplay videos. Pipe up on the HTML Purifier forums if this - is a capability you want. -

---# vim: et sw=4 sts=4 +URI.SafeIframeRegexp +TYPE: string/null +VERSION: 4.4.0 +DEFAULT: NULL +--DESCRIPTION-- +

+ A PCRE regular expression that will be matched against an iframe URI. This is + a relatively inflexible scheme, but works well enough for the most common + use-case of iframes: embedded video. This directive only has an effect if + %HTML.SafeIframe is enabled. Here are some example values: +

+
    +
  • %^http://www.youtube.com/embed/% - Allow YouTube videos
  • +
  • %^http://player.vimeo.com/video/% - Allow Vimeo videos
  • +
  • %^http://(www.youtube.com/embed/|player.vimeo.com/video/)% - Allow both
  • +
+

+ Note that this directive does not give you enough granularity to, say, disable + all autoplay videos. Pipe up on the HTML Purifier forums if this + is a capability you want. +

+--# vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ConfigSchema/schema/info.ini b/lib/purifier/HTMLPurifier/ConfigSchema/schema/info.ini index 58e0ce4a17..5de4505e1b 100644 --- a/lib/purifier/HTMLPurifier/ConfigSchema/schema/info.ini +++ b/lib/purifier/HTMLPurifier/ConfigSchema/schema/info.ini @@ -1,3 +1,3 @@ -name = "HTML Purifier" - -; vim: et sw=4 sts=4 +name = "HTML Purifier" + +; vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ContentSets.php b/lib/purifier/HTMLPurifier/ContentSets.php index f68b19631a..543e3f8f11 100644 --- a/lib/purifier/HTMLPurifier/ContentSets.php +++ b/lib/purifier/HTMLPurifier/ContentSets.php @@ -1,170 +1,170 @@ - true) indexed by name. - * @type array - * @note This is in HTMLPurifier_HTMLDefinition->info_content_sets - */ - public $lookup = array(); - - /** - * Synchronized list of defined content sets (keys of info). - * @type array - */ - protected $keys = array(); - /** - * Synchronized list of defined content values (values of info). - * @type array - */ - protected $values = array(); - - /** - * Merges in module's content sets, expands identifiers in the content - * sets and populates the keys, values and lookup member variables. - * @param HTMLPurifier_HTMLModule[] $modules List of HTMLPurifier_HTMLModule - */ - public function __construct($modules) - { - if (!is_array($modules)) { - $modules = array($modules); - } - // populate content_sets based on module hints - // sorry, no way of overloading - foreach ($modules as $module) { - foreach ($module->content_sets as $key => $value) { - $temp = $this->convertToLookup($value); - if (isset($this->lookup[$key])) { - // add it into the existing content set - $this->lookup[$key] = array_merge($this->lookup[$key], $temp); - } else { - $this->lookup[$key] = $temp; - } - } - } - $old_lookup = false; - while ($old_lookup !== $this->lookup) { - $old_lookup = $this->lookup; - foreach ($this->lookup as $i => $set) { - $add = array(); - foreach ($set as $element => $x) { - if (isset($this->lookup[$element])) { - $add += $this->lookup[$element]; - unset($this->lookup[$i][$element]); - } - } - $this->lookup[$i] += $add; - } - } - - foreach ($this->lookup as $key => $lookup) { - $this->info[$key] = implode(' | ', array_keys($lookup)); - } - $this->keys = array_keys($this->info); - $this->values = array_values($this->info); - } - - /** - * Accepts a definition; generates and assigns a ChildDef for it - * @param HTMLPurifier_ElementDef $def HTMLPurifier_ElementDef reference - * @param HTMLPurifier_HTMLModule $module Module that defined the ElementDef - */ - public function generateChildDef(&$def, $module) - { - if (!empty($def->child)) { // already done! - return; - } - $content_model = $def->content_model; - if (is_string($content_model)) { - // Assume that $this->keys is alphanumeric - $def->content_model = preg_replace_callback( - '/\b(' . implode('|', $this->keys) . ')\b/', - array($this, 'generateChildDefCallback'), - $content_model - ); - //$def->content_model = str_replace( - // $this->keys, $this->values, $content_model); - } - $def->child = $this->getChildDef($def, $module); - } - - public function generateChildDefCallback($matches) - { - return $this->info[$matches[0]]; - } - - /** - * Instantiates a ChildDef based on content_model and content_model_type - * member variables in HTMLPurifier_ElementDef - * @note This will also defer to modules for custom HTMLPurifier_ChildDef - * subclasses that need content set expansion - * @param HTMLPurifier_ElementDef $def HTMLPurifier_ElementDef to have ChildDef extracted - * @param HTMLPurifier_HTMLModule $module Module that defined the ElementDef - * @return HTMLPurifier_ChildDef corresponding to ElementDef - */ - public function getChildDef($def, $module) - { - $value = $def->content_model; - if (is_object($value)) { - trigger_error( - 'Literal object child definitions should be stored in '. - 'ElementDef->child not ElementDef->content_model', - E_USER_NOTICE - ); - return $value; - } - switch ($def->content_model_type) { - case 'required': - return new HTMLPurifier_ChildDef_Required($value); - case 'optional': - return new HTMLPurifier_ChildDef_Optional($value); - case 'empty': - return new HTMLPurifier_ChildDef_Empty(); - case 'custom': - return new HTMLPurifier_ChildDef_Custom($value); - } - // defer to its module - $return = false; - if ($module->defines_child_def) { // save a func call - $return = $module->getChildDef($def); - } - if ($return !== false) { - return $return; - } - // error-out - trigger_error( - 'Could not determine which ChildDef class to instantiate', - E_USER_ERROR - ); - return false; - } - - /** - * Converts a string list of elements separated by pipes into - * a lookup array. - * @param string $string List of elements - * @return array Lookup array of elements - */ - protected function convertToLookup($string) - { - $array = explode('|', str_replace(' ', '', $string)); - $ret = array(); - foreach ($array as $k) { - $ret[$k] = true; - } - return $ret; - } -} - -// vim: et sw=4 sts=4 + true) indexed by name. + * @type array + * @note This is in HTMLPurifier_HTMLDefinition->info_content_sets + */ + public $lookup = array(); + + /** + * Synchronized list of defined content sets (keys of info). + * @type array + */ + protected $keys = array(); + /** + * Synchronized list of defined content values (values of info). + * @type array + */ + protected $values = array(); + + /** + * Merges in module's content sets, expands identifiers in the content + * sets and populates the keys, values and lookup member variables. + * @param HTMLPurifier_HTMLModule[] $modules List of HTMLPurifier_HTMLModule + */ + public function __construct($modules) + { + if (!is_array($modules)) { + $modules = array($modules); + } + // populate content_sets based on module hints + // sorry, no way of overloading + foreach ($modules as $module) { + foreach ($module->content_sets as $key => $value) { + $temp = $this->convertToLookup($value); + if (isset($this->lookup[$key])) { + // add it into the existing content set + $this->lookup[$key] = array_merge($this->lookup[$key], $temp); + } else { + $this->lookup[$key] = $temp; + } + } + } + $old_lookup = false; + while ($old_lookup !== $this->lookup) { + $old_lookup = $this->lookup; + foreach ($this->lookup as $i => $set) { + $add = array(); + foreach ($set as $element => $x) { + if (isset($this->lookup[$element])) { + $add += $this->lookup[$element]; + unset($this->lookup[$i][$element]); + } + } + $this->lookup[$i] += $add; + } + } + + foreach ($this->lookup as $key => $lookup) { + $this->info[$key] = implode(' | ', array_keys($lookup)); + } + $this->keys = array_keys($this->info); + $this->values = array_values($this->info); + } + + /** + * Accepts a definition; generates and assigns a ChildDef for it + * @param HTMLPurifier_ElementDef $def HTMLPurifier_ElementDef reference + * @param HTMLPurifier_HTMLModule $module Module that defined the ElementDef + */ + public function generateChildDef(&$def, $module) + { + if (!empty($def->child)) { // already done! + return; + } + $content_model = $def->content_model; + if (is_string($content_model)) { + // Assume that $this->keys is alphanumeric + $def->content_model = preg_replace_callback( + '/\b(' . implode('|', $this->keys) . ')\b/', + array($this, 'generateChildDefCallback'), + $content_model + ); + //$def->content_model = str_replace( + // $this->keys, $this->values, $content_model); + } + $def->child = $this->getChildDef($def, $module); + } + + public function generateChildDefCallback($matches) + { + return $this->info[$matches[0]]; + } + + /** + * Instantiates a ChildDef based on content_model and content_model_type + * member variables in HTMLPurifier_ElementDef + * @note This will also defer to modules for custom HTMLPurifier_ChildDef + * subclasses that need content set expansion + * @param HTMLPurifier_ElementDef $def HTMLPurifier_ElementDef to have ChildDef extracted + * @param HTMLPurifier_HTMLModule $module Module that defined the ElementDef + * @return HTMLPurifier_ChildDef corresponding to ElementDef + */ + public function getChildDef($def, $module) + { + $value = $def->content_model; + if (is_object($value)) { + trigger_error( + 'Literal object child definitions should be stored in '. + 'ElementDef->child not ElementDef->content_model', + E_USER_NOTICE + ); + return $value; + } + switch ($def->content_model_type) { + case 'required': + return new HTMLPurifier_ChildDef_Required($value); + case 'optional': + return new HTMLPurifier_ChildDef_Optional($value); + case 'empty': + return new HTMLPurifier_ChildDef_Empty(); + case 'custom': + return new HTMLPurifier_ChildDef_Custom($value); + } + // defer to its module + $return = false; + if ($module->defines_child_def) { // save a func call + $return = $module->getChildDef($def); + } + if ($return !== false) { + return $return; + } + // error-out + trigger_error( + 'Could not determine which ChildDef class to instantiate', + E_USER_ERROR + ); + return false; + } + + /** + * Converts a string list of elements separated by pipes into + * a lookup array. + * @param string $string List of elements + * @return array Lookup array of elements + */ + protected function convertToLookup($string) + { + $array = explode('|', str_replace(' ', '', $string)); + $ret = array(); + foreach ($array as $k) { + $ret[$k] = true; + } + return $ret; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/Context.php b/lib/purifier/HTMLPurifier/Context.php index 5ad536d51c..00e509c85c 100644 --- a/lib/purifier/HTMLPurifier/Context.php +++ b/lib/purifier/HTMLPurifier/Context.php @@ -1,95 +1,95 @@ -_storage)) { - trigger_error( - "Name $name produces collision, cannot re-register", - E_USER_ERROR - ); - return; - } - $this->_storage[$name] =& $ref; - } - - /** - * Retrieves a variable reference from the context. - * @param string $name String name - * @param bool $ignore_error Boolean whether or not to ignore error - * @return mixed - */ - public function &get($name, $ignore_error = false) - { - if (!array_key_exists($name, $this->_storage)) { - if (!$ignore_error) { - trigger_error( - "Attempted to retrieve non-existent variable $name", - E_USER_ERROR - ); - } - $var = null; // so we can return by reference - return $var; - } - return $this->_storage[$name]; - } - - /** - * Destroys a variable in the context. - * @param string $name String name - */ - public function destroy($name) - { - if (!array_key_exists($name, $this->_storage)) { - trigger_error( - "Attempted to destroy non-existent variable $name", - E_USER_ERROR - ); - return; - } - unset($this->_storage[$name]); - } - - /** - * Checks whether or not the variable exists. - * @param string $name String name - * @return bool - */ - public function exists($name) - { - return array_key_exists($name, $this->_storage); - } - - /** - * Loads a series of variables from an associative array - * @param array $context_array Assoc array of variables to load - */ - public function loadArray($context_array) - { - foreach ($context_array as $key => $discard) { - $this->register($key, $context_array[$key]); - } - } -} - -// vim: et sw=4 sts=4 +_storage)) { + trigger_error( + "Name $name produces collision, cannot re-register", + E_USER_ERROR + ); + return; + } + $this->_storage[$name] =& $ref; + } + + /** + * Retrieves a variable reference from the context. + * @param string $name String name + * @param bool $ignore_error Boolean whether or not to ignore error + * @return mixed + */ + public function &get($name, $ignore_error = false) + { + if (!array_key_exists($name, $this->_storage)) { + if (!$ignore_error) { + trigger_error( + "Attempted to retrieve non-existent variable $name", + E_USER_ERROR + ); + } + $var = null; // so we can return by reference + return $var; + } + return $this->_storage[$name]; + } + + /** + * Destroys a variable in the context. + * @param string $name String name + */ + public function destroy($name) + { + if (!array_key_exists($name, $this->_storage)) { + trigger_error( + "Attempted to destroy non-existent variable $name", + E_USER_ERROR + ); + return; + } + unset($this->_storage[$name]); + } + + /** + * Checks whether or not the variable exists. + * @param string $name String name + * @return bool + */ + public function exists($name) + { + return array_key_exists($name, $this->_storage); + } + + /** + * Loads a series of variables from an associative array + * @param array $context_array Assoc array of variables to load + */ + public function loadArray($context_array) + { + foreach ($context_array as $key => $discard) { + $this->register($key, $context_array[$key]); + } + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/Definition.php b/lib/purifier/HTMLPurifier/Definition.php index f987e60359..bc6d433647 100644 --- a/lib/purifier/HTMLPurifier/Definition.php +++ b/lib/purifier/HTMLPurifier/Definition.php @@ -1,55 +1,55 @@ -setup) { - return; - } - $this->setup = true; - $this->doSetup($config); - } -} - -// vim: et sw=4 sts=4 +setup) { + return; + } + $this->setup = true; + $this->doSetup($config); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/DefinitionCache.php b/lib/purifier/HTMLPurifier/DefinitionCache.php index 72448d03fc..9aa8ff354f 100644 --- a/lib/purifier/HTMLPurifier/DefinitionCache.php +++ b/lib/purifier/HTMLPurifier/DefinitionCache.php @@ -1,129 +1,129 @@ -type = $type; - } - - /** - * Generates a unique identifier for a particular configuration - * @param HTMLPurifier_Config $config Instance of HTMLPurifier_Config - * @return string - */ - public function generateKey($config) - { - return $config->version . ',' . // possibly replace with function calls - $config->getBatchSerial($this->type) . ',' . - $config->get($this->type . '.DefinitionRev'); - } - - /** - * Tests whether or not a key is old with respect to the configuration's - * version and revision number. - * @param string $key Key to test - * @param HTMLPurifier_Config $config Instance of HTMLPurifier_Config to test against - * @return bool - */ - public function isOld($key, $config) - { - if (substr_count($key, ',') < 2) { - return true; - } - list($version, $hash, $revision) = explode(',', $key, 3); - $compare = version_compare($version, $config->version); - // version mismatch, is always old - if ($compare != 0) { - return true; - } - // versions match, ids match, check revision number - if ($hash == $config->getBatchSerial($this->type) && - $revision < $config->get($this->type . '.DefinitionRev')) { - return true; - } - return false; - } - - /** - * Checks if a definition's type jives with the cache's type - * @note Throws an error on failure - * @param HTMLPurifier_Definition $def Definition object to check - * @return bool true if good, false if not - */ - public function checkDefType($def) - { - if ($def->type !== $this->type) { - trigger_error("Cannot use definition of type {$def->type} in cache for {$this->type}"); - return false; - } - return true; - } - - /** - * Adds a definition object to the cache - * @param HTMLPurifier_Definition $def - * @param HTMLPurifier_Config $config - */ - abstract public function add($def, $config); - - /** - * Unconditionally saves a definition object to the cache - * @param HTMLPurifier_Definition $def - * @param HTMLPurifier_Config $config - */ - abstract public function set($def, $config); - - /** - * Replace an object in the cache - * @param HTMLPurifier_Definition $def - * @param HTMLPurifier_Config $config - */ - abstract public function replace($def, $config); - - /** - * Retrieves a definition object from the cache - * @param HTMLPurifier_Config $config - */ - abstract public function get($config); - - /** - * Removes a definition object to the cache - * @param HTMLPurifier_Config $config - */ - abstract public function remove($config); - - /** - * Clears all objects from cache - * @param HTMLPurifier_Config $config - */ - abstract public function flush($config); - - /** - * Clears all expired (older version or revision) objects from cache - * @note Be careful implementing this method as flush. Flush must - * not interfere with other Definition types, and cleanup() - * should not be repeatedly called by userland code. - * @param HTMLPurifier_Config $config - */ - abstract public function cleanup($config); -} - -// vim: et sw=4 sts=4 +type = $type; + } + + /** + * Generates a unique identifier for a particular configuration + * @param HTMLPurifier_Config $config Instance of HTMLPurifier_Config + * @return string + */ + public function generateKey($config) + { + return $config->version . ',' . // possibly replace with function calls + $config->getBatchSerial($this->type) . ',' . + $config->get($this->type . '.DefinitionRev'); + } + + /** + * Tests whether or not a key is old with respect to the configuration's + * version and revision number. + * @param string $key Key to test + * @param HTMLPurifier_Config $config Instance of HTMLPurifier_Config to test against + * @return bool + */ + public function isOld($key, $config) + { + if (substr_count($key, ',') < 2) { + return true; + } + list($version, $hash, $revision) = explode(',', $key, 3); + $compare = version_compare($version, $config->version); + // version mismatch, is always old + if ($compare != 0) { + return true; + } + // versions match, ids match, check revision number + if ($hash == $config->getBatchSerial($this->type) && + $revision < $config->get($this->type . '.DefinitionRev')) { + return true; + } + return false; + } + + /** + * Checks if a definition's type jives with the cache's type + * @note Throws an error on failure + * @param HTMLPurifier_Definition $def Definition object to check + * @return bool true if good, false if not + */ + public function checkDefType($def) + { + if ($def->type !== $this->type) { + trigger_error("Cannot use definition of type {$def->type} in cache for {$this->type}"); + return false; + } + return true; + } + + /** + * Adds a definition object to the cache + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + */ + abstract public function add($def, $config); + + /** + * Unconditionally saves a definition object to the cache + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + */ + abstract public function set($def, $config); + + /** + * Replace an object in the cache + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + */ + abstract public function replace($def, $config); + + /** + * Retrieves a definition object from the cache + * @param HTMLPurifier_Config $config + */ + abstract public function get($config); + + /** + * Removes a definition object to the cache + * @param HTMLPurifier_Config $config + */ + abstract public function remove($config); + + /** + * Clears all objects from cache + * @param HTMLPurifier_Config $config + */ + abstract public function flush($config); + + /** + * Clears all expired (older version or revision) objects from cache + * @note Be careful implementing this method as flush. Flush must + * not interfere with other Definition types, and cleanup() + * should not be repeatedly called by userland code. + * @param HTMLPurifier_Config $config + */ + abstract public function cleanup($config); +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/DefinitionCache/Decorator.php b/lib/purifier/HTMLPurifier/DefinitionCache/Decorator.php index 09cdc1c825..b57a51b6cb 100644 --- a/lib/purifier/HTMLPurifier/DefinitionCache/Decorator.php +++ b/lib/purifier/HTMLPurifier/DefinitionCache/Decorator.php @@ -1,112 +1,112 @@ -copy(); - // reference is necessary for mocks in PHP 4 - $decorator->cache =& $cache; - $decorator->type = $cache->type; - return $decorator; - } - - /** - * Cross-compatible clone substitute - * @return HTMLPurifier_DefinitionCache_Decorator - */ - public function copy() - { - return new HTMLPurifier_DefinitionCache_Decorator(); - } - - /** - * @param HTMLPurifier_Definition $def - * @param HTMLPurifier_Config $config - * @return mixed - */ - public function add($def, $config) - { - return $this->cache->add($def, $config); - } - - /** - * @param HTMLPurifier_Definition $def - * @param HTMLPurifier_Config $config - * @return mixed - */ - public function set($def, $config) - { - return $this->cache->set($def, $config); - } - - /** - * @param HTMLPurifier_Definition $def - * @param HTMLPurifier_Config $config - * @return mixed - */ - public function replace($def, $config) - { - return $this->cache->replace($def, $config); - } - - /** - * @param HTMLPurifier_Config $config - * @return mixed - */ - public function get($config) - { - return $this->cache->get($config); - } - - /** - * @param HTMLPurifier_Config $config - * @return mixed - */ - public function remove($config) - { - return $this->cache->remove($config); - } - - /** - * @param HTMLPurifier_Config $config - * @return mixed - */ - public function flush($config) - { - return $this->cache->flush($config); - } - - /** - * @param HTMLPurifier_Config $config - * @return mixed - */ - public function cleanup($config) - { - return $this->cache->cleanup($config); - } -} - -// vim: et sw=4 sts=4 +copy(); + // reference is necessary for mocks in PHP 4 + $decorator->cache =& $cache; + $decorator->type = $cache->type; + return $decorator; + } + + /** + * Cross-compatible clone substitute + * @return HTMLPurifier_DefinitionCache_Decorator + */ + public function copy() + { + return new HTMLPurifier_DefinitionCache_Decorator(); + } + + /** + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function add($def, $config) + { + return $this->cache->add($def, $config); + } + + /** + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function set($def, $config) + { + return $this->cache->set($def, $config); + } + + /** + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function replace($def, $config) + { + return $this->cache->replace($def, $config); + } + + /** + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function get($config) + { + return $this->cache->get($config); + } + + /** + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function remove($config) + { + return $this->cache->remove($config); + } + + /** + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function flush($config) + { + return $this->cache->flush($config); + } + + /** + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function cleanup($config) + { + return $this->cache->cleanup($config); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php b/lib/purifier/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php index 53dca67dcd..4991777ce1 100644 --- a/lib/purifier/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php +++ b/lib/purifier/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php @@ -1,78 +1,78 @@ -definitions[$this->generateKey($config)] = $def; - } - return $status; - } - - /** - * @param HTMLPurifier_Definition $def - * @param HTMLPurifier_Config $config - * @return mixed - */ - public function set($def, $config) - { - $status = parent::set($def, $config); - if ($status) { - $this->definitions[$this->generateKey($config)] = $def; - } - return $status; - } - - /** - * @param HTMLPurifier_Definition $def - * @param HTMLPurifier_Config $config - * @return mixed - */ - public function replace($def, $config) - { - $status = parent::replace($def, $config); - if ($status) { - $this->definitions[$this->generateKey($config)] = $def; - } - return $status; - } - - /** - * @param HTMLPurifier_Config $config - * @return mixed - */ - public function get($config) - { - $key = $this->generateKey($config); - if (isset($this->definitions[$key])) { - return $this->definitions[$key]; - } - $this->definitions[$key] = parent::get($config); - return $this->definitions[$key]; - } -} - -// vim: et sw=4 sts=4 +definitions[$this->generateKey($config)] = $def; + } + return $status; + } + + /** + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function set($def, $config) + { + $status = parent::set($def, $config); + if ($status) { + $this->definitions[$this->generateKey($config)] = $def; + } + return $status; + } + + /** + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function replace($def, $config) + { + $status = parent::replace($def, $config); + if ($status) { + $this->definitions[$this->generateKey($config)] = $def; + } + return $status; + } + + /** + * @param HTMLPurifier_Config $config + * @return mixed + */ + public function get($config) + { + $key = $this->generateKey($config); + if (isset($this->definitions[$key])) { + return $this->definitions[$key]; + } + $this->definitions[$key] = parent::get($config); + return $this->definitions[$key]; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/DefinitionCache/Decorator/Template.php.in b/lib/purifier/HTMLPurifier/DefinitionCache/Decorator/Template.php.in index c586890fbc..b1fec8d367 100644 --- a/lib/purifier/HTMLPurifier/DefinitionCache/Decorator/Template.php.in +++ b/lib/purifier/HTMLPurifier/DefinitionCache/Decorator/Template.php.in @@ -1,82 +1,82 @@ -checkDefType($def)) { - return; - } - $file = $this->generateFilePath($config); - if (file_exists($file)) { - return false; - } - if (!$this->_prepareDir($config)) { - return false; - } - return $this->_write($file, serialize($def), $config); - } - - /** - * @param HTMLPurifier_Definition $def - * @param HTMLPurifier_Config $config - * @return int|bool - */ - public function set($def, $config) - { - if (!$this->checkDefType($def)) { - return; - } - $file = $this->generateFilePath($config); - if (!$this->_prepareDir($config)) { - return false; - } - return $this->_write($file, serialize($def), $config); - } - - /** - * @param HTMLPurifier_Definition $def - * @param HTMLPurifier_Config $config - * @return int|bool - */ - public function replace($def, $config) - { - if (!$this->checkDefType($def)) { - return; - } - $file = $this->generateFilePath($config); - if (!file_exists($file)) { - return false; - } - if (!$this->_prepareDir($config)) { - return false; - } - return $this->_write($file, serialize($def), $config); - } - - /** - * @param HTMLPurifier_Config $config - * @return bool|HTMLPurifier_Config - */ - public function get($config) - { - $file = $this->generateFilePath($config); - if (!file_exists($file)) { - return false; - } - return unserialize(file_get_contents($file)); - } - - /** - * @param HTMLPurifier_Config $config - * @return bool - */ - public function remove($config) - { - $file = $this->generateFilePath($config); - if (!file_exists($file)) { - return false; - } - return unlink($file); - } - - /** - * @param HTMLPurifier_Config $config - * @return bool - */ - public function flush($config) - { - if (!$this->_prepareDir($config)) { - return false; - } - $dir = $this->generateDirectoryPath($config); - $dh = opendir($dir); - // Apparently, on some versions of PHP, readdir will return - // an empty string if you pass an invalid argument to readdir. - // So you need this test. See #49. - if (false === $dh) { - return false; - } - while (false !== ($filename = readdir($dh))) { - if (empty($filename)) { - continue; - } - if ($filename[0] === '.') { - continue; - } - unlink($dir . '/' . $filename); - } - closedir($dh); - return true; - } - - /** - * @param HTMLPurifier_Config $config - * @return bool - */ - public function cleanup($config) - { - if (!$this->_prepareDir($config)) { - return false; - } - $dir = $this->generateDirectoryPath($config); - $dh = opendir($dir); - // See #49 (and above). - if (false === $dh) { - return false; - } - while (false !== ($filename = readdir($dh))) { - if (empty($filename)) { - continue; - } - if ($filename[0] === '.') { - continue; - } - $key = substr($filename, 0, strlen($filename) - 4); - if ($this->isOld($key, $config)) { - unlink($dir . '/' . $filename); - } - } - closedir($dh); - return true; - } - - /** - * Generates the file path to the serial file corresponding to - * the configuration and definition name - * @param HTMLPurifier_Config $config - * @return string - * @todo Make protected - */ - public function generateFilePath($config) - { - $key = $this->generateKey($config); - return $this->generateDirectoryPath($config) . '/' . $key . '.ser'; - } - - /** - * Generates the path to the directory contain this cache's serial files - * @param HTMLPurifier_Config $config - * @return string - * @note No trailing slash - * @todo Make protected - */ - public function generateDirectoryPath($config) - { - $base = $this->generateBaseDirectoryPath($config); - return $base . '/' . $this->type; - } - - /** - * Generates path to base directory that contains all definition type - * serials - * @param HTMLPurifier_Config $config - * @return mixed|string - * @todo Make protected - */ - public function generateBaseDirectoryPath($config) - { - $base = $config->get('Cache.SerializerPath'); - $base = is_null($base) ? HTMLPURIFIER_PREFIX . '/HTMLPurifier/DefinitionCache/Serializer' : $base; - return $base; - } - - /** - * Convenience wrapper function for file_put_contents - * @param string $file File name to write to - * @param string $data Data to write into file - * @param HTMLPurifier_Config $config - * @return int|bool Number of bytes written if success, or false if failure. - */ - private function _write($file, $data, $config) - { - $result = file_put_contents($file, $data); - if ($result !== false) { - // set permissions of the new file (no execute) - $chmod = $config->get('Cache.SerializerPermissions'); - if ($chmod !== null) { - chmod($file, $chmod & 0666); - } - } - return $result; - } - - /** - * Prepares the directory that this type stores the serials in - * @param HTMLPurifier_Config $config - * @return bool True if successful - */ - private function _prepareDir($config) - { - $directory = $this->generateDirectoryPath($config); - $chmod = $config->get('Cache.SerializerPermissions'); - if ($chmod === null) { - // TODO: This races - if (is_dir($directory)) return true; - return mkdir($directory); - } - if (!is_dir($directory)) { - $base = $this->generateBaseDirectoryPath($config); - if (!is_dir($base)) { - trigger_error( - 'Base directory ' . $base . ' does not exist, - please create or change using %Cache.SerializerPath', - E_USER_WARNING - ); - return false; - } elseif (!$this->_testPermissions($base, $chmod)) { - return false; - } - if (!mkdir($directory, $chmod)) { - trigger_error( - 'Could not create directory ' . $directory . '', - E_USER_WARNING - ); - return false; - } - if (!$this->_testPermissions($directory, $chmod)) { - return false; - } - } elseif (!$this->_testPermissions($directory, $chmod)) { - return false; - } - return true; - } - - /** - * Tests permissions on a directory and throws out friendly - * error messages and attempts to chmod it itself if possible - * @param string $dir Directory path - * @param int $chmod Permissions - * @return bool True if directory is writable - */ - private function _testPermissions($dir, $chmod) - { - // early abort, if it is writable, everything is hunky-dory - if (is_writable($dir)) { - return true; - } - if (!is_dir($dir)) { - // generally, you'll want to handle this beforehand - // so a more specific error message can be given - trigger_error( - 'Directory ' . $dir . ' does not exist', - E_USER_WARNING - ); - return false; - } - if (function_exists('posix_getuid') && $chmod !== null) { - // POSIX system, we can give more specific advice - if (fileowner($dir) === posix_getuid()) { - // we can chmod it ourselves - $chmod = $chmod | 0700; - if (chmod($dir, $chmod)) { - return true; - } - } elseif (filegroup($dir) === posix_getgid()) { - $chmod = $chmod | 0070; - } else { - // PHP's probably running as nobody, so we'll - // need to give global permissions - $chmod = $chmod | 0777; - } - trigger_error( - 'Directory ' . $dir . ' not writable, ' . - 'please chmod to ' . decoct($chmod), - E_USER_WARNING - ); - } else { - // generic error message - trigger_error( - 'Directory ' . $dir . ' not writable, ' . - 'please alter file permissions', - E_USER_WARNING - ); - } - return false; - } -} - -// vim: et sw=4 sts=4 +checkDefType($def)) { + return; + } + $file = $this->generateFilePath($config); + if (file_exists($file)) { + return false; + } + if (!$this->_prepareDir($config)) { + return false; + } + return $this->_write($file, serialize($def), $config); + } + + /** + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + * @return int|bool + */ + public function set($def, $config) + { + if (!$this->checkDefType($def)) { + return; + } + $file = $this->generateFilePath($config); + if (!$this->_prepareDir($config)) { + return false; + } + return $this->_write($file, serialize($def), $config); + } + + /** + * @param HTMLPurifier_Definition $def + * @param HTMLPurifier_Config $config + * @return int|bool + */ + public function replace($def, $config) + { + if (!$this->checkDefType($def)) { + return; + } + $file = $this->generateFilePath($config); + if (!file_exists($file)) { + return false; + } + if (!$this->_prepareDir($config)) { + return false; + } + return $this->_write($file, serialize($def), $config); + } + + /** + * @param HTMLPurifier_Config $config + * @return bool|HTMLPurifier_Config + */ + public function get($config) + { + $file = $this->generateFilePath($config); + if (!file_exists($file)) { + return false; + } + return unserialize(file_get_contents($file)); + } + + /** + * @param HTMLPurifier_Config $config + * @return bool + */ + public function remove($config) + { + $file = $this->generateFilePath($config); + if (!file_exists($file)) { + return false; + } + return unlink($file); + } + + /** + * @param HTMLPurifier_Config $config + * @return bool + */ + public function flush($config) + { + if (!$this->_prepareDir($config)) { + return false; + } + $dir = $this->generateDirectoryPath($config); + $dh = opendir($dir); + // Apparently, on some versions of PHP, readdir will return + // an empty string if you pass an invalid argument to readdir. + // So you need this test. See #49. + if (false === $dh) { + return false; + } + while (false !== ($filename = readdir($dh))) { + if (empty($filename)) { + continue; + } + if ($filename[0] === '.') { + continue; + } + unlink($dir . '/' . $filename); + } + closedir($dh); + return true; + } + + /** + * @param HTMLPurifier_Config $config + * @return bool + */ + public function cleanup($config) + { + if (!$this->_prepareDir($config)) { + return false; + } + $dir = $this->generateDirectoryPath($config); + $dh = opendir($dir); + // See #49 (and above). + if (false === $dh) { + return false; + } + while (false !== ($filename = readdir($dh))) { + if (empty($filename)) { + continue; + } + if ($filename[0] === '.') { + continue; + } + $key = substr($filename, 0, strlen($filename) - 4); + if ($this->isOld($key, $config)) { + unlink($dir . '/' . $filename); + } + } + closedir($dh); + return true; + } + + /** + * Generates the file path to the serial file corresponding to + * the configuration and definition name + * @param HTMLPurifier_Config $config + * @return string + * @todo Make protected + */ + public function generateFilePath($config) + { + $key = $this->generateKey($config); + return $this->generateDirectoryPath($config) . '/' . $key . '.ser'; + } + + /** + * Generates the path to the directory contain this cache's serial files + * @param HTMLPurifier_Config $config + * @return string + * @note No trailing slash + * @todo Make protected + */ + public function generateDirectoryPath($config) + { + $base = $this->generateBaseDirectoryPath($config); + return $base . '/' . $this->type; + } + + /** + * Generates path to base directory that contains all definition type + * serials + * @param HTMLPurifier_Config $config + * @return mixed|string + * @todo Make protected + */ + public function generateBaseDirectoryPath($config) + { + $base = $config->get('Cache.SerializerPath'); + $base = is_null($base) ? HTMLPURIFIER_PREFIX . '/HTMLPurifier/DefinitionCache/Serializer' : $base; + return $base; + } + + /** + * Convenience wrapper function for file_put_contents + * @param string $file File name to write to + * @param string $data Data to write into file + * @param HTMLPurifier_Config $config + * @return int|bool Number of bytes written if success, or false if failure. + */ + private function _write($file, $data, $config) + { + $result = file_put_contents($file, $data); + if ($result !== false) { + // set permissions of the new file (no execute) + $chmod = $config->get('Cache.SerializerPermissions'); + if ($chmod !== null) { + chmod($file, $chmod & 0666); + } + } + return $result; + } + + /** + * Prepares the directory that this type stores the serials in + * @param HTMLPurifier_Config $config + * @return bool True if successful + */ + private function _prepareDir($config) + { + $directory = $this->generateDirectoryPath($config); + $chmod = $config->get('Cache.SerializerPermissions'); + if ($chmod === null) { + if (!@mkdir($directory) && !is_dir($directory)) { + trigger_error( + 'Could not create directory ' . $directory . '', + E_USER_WARNING + ); + return false; + } + return true; + } + if (!is_dir($directory)) { + $base = $this->generateBaseDirectoryPath($config); + if (!is_dir($base)) { + trigger_error( + 'Base directory ' . $base . ' does not exist, + please create or change using %Cache.SerializerPath', + E_USER_WARNING + ); + return false; + } elseif (!$this->_testPermissions($base, $chmod)) { + return false; + } + if (!@mkdir($directory, $chmod) && !is_dir($directory)) { + trigger_error( + 'Could not create directory ' . $directory . '', + E_USER_WARNING + ); + return false; + } + if (!$this->_testPermissions($directory, $chmod)) { + return false; + } + } elseif (!$this->_testPermissions($directory, $chmod)) { + return false; + } + return true; + } + + /** + * Tests permissions on a directory and throws out friendly + * error messages and attempts to chmod it itself if possible + * @param string $dir Directory path + * @param int $chmod Permissions + * @return bool True if directory is writable + */ + private function _testPermissions($dir, $chmod) + { + // early abort, if it is writable, everything is hunky-dory + if (is_writable($dir)) { + return true; + } + if (!is_dir($dir)) { + // generally, you'll want to handle this beforehand + // so a more specific error message can be given + trigger_error( + 'Directory ' . $dir . ' does not exist', + E_USER_WARNING + ); + return false; + } + if (function_exists('posix_getuid') && $chmod !== null) { + // POSIX system, we can give more specific advice + if (fileowner($dir) === posix_getuid()) { + // we can chmod it ourselves + $chmod = $chmod | 0700; + if (chmod($dir, $chmod)) { + return true; + } + } elseif (filegroup($dir) === posix_getgid()) { + $chmod = $chmod | 0070; + } else { + // PHP's probably running as nobody, so we'll + // need to give global permissions + $chmod = $chmod | 0777; + } + trigger_error( + 'Directory ' . $dir . ' not writable, ' . + 'please chmod to ' . decoct($chmod), + E_USER_WARNING + ); + } else { + // generic error message + trigger_error( + 'Directory ' . $dir . ' not writable, ' . + 'please alter file permissions', + E_USER_WARNING + ); + } + return false; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/DefinitionCache/Serializer/README b/lib/purifier/HTMLPurifier/DefinitionCache/Serializer/README index ba005de737..2e35c1c3d0 100644 --- a/lib/purifier/HTMLPurifier/DefinitionCache/Serializer/README +++ b/lib/purifier/HTMLPurifier/DefinitionCache/Serializer/README @@ -1,3 +1,3 @@ -This is a dummy file to prevent Git from ignoring this empty directory. - - vim: et sw=4 sts=4 +This is a dummy file to prevent Git from ignoring this empty directory. + + vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/DefinitionCacheFactory.php b/lib/purifier/HTMLPurifier/DefinitionCacheFactory.php index 737b8bb3b0..fd1cc9be46 100644 --- a/lib/purifier/HTMLPurifier/DefinitionCacheFactory.php +++ b/lib/purifier/HTMLPurifier/DefinitionCacheFactory.php @@ -1,106 +1,106 @@ - array()); - - /** - * @type array - */ - protected $implementations = array(); - - /** - * @type HTMLPurifier_DefinitionCache_Decorator[] - */ - protected $decorators = array(); - - /** - * Initialize default decorators - */ - public function setup() - { - $this->addDecorator('Cleanup'); - } - - /** - * Retrieves an instance of global definition cache factory. - * @param HTMLPurifier_DefinitionCacheFactory $prototype - * @return HTMLPurifier_DefinitionCacheFactory - */ - public static function instance($prototype = null) - { - static $instance; - if ($prototype !== null) { - $instance = $prototype; - } elseif ($instance === null || $prototype === true) { - $instance = new HTMLPurifier_DefinitionCacheFactory(); - $instance->setup(); - } - return $instance; - } - - /** - * Registers a new definition cache object - * @param string $short Short name of cache object, for reference - * @param string $long Full class name of cache object, for construction - */ - public function register($short, $long) - { - $this->implementations[$short] = $long; - } - - /** - * Factory method that creates a cache object based on configuration - * @param string $type Name of definitions handled by cache - * @param HTMLPurifier_Config $config Config instance - * @return mixed - */ - public function create($type, $config) - { - $method = $config->get('Cache.DefinitionImpl'); - if ($method === null) { - return new HTMLPurifier_DefinitionCache_Null($type); - } - if (!empty($this->caches[$method][$type])) { - return $this->caches[$method][$type]; - } - if (isset($this->implementations[$method]) && - class_exists($class = $this->implementations[$method], false)) { - $cache = new $class($type); - } else { - if ($method != 'Serializer') { - trigger_error("Unrecognized DefinitionCache $method, using Serializer instead", E_USER_WARNING); - } - $cache = new HTMLPurifier_DefinitionCache_Serializer($type); - } - foreach ($this->decorators as $decorator) { - $new_cache = $decorator->decorate($cache); - // prevent infinite recursion in PHP 4 - unset($cache); - $cache = $new_cache; - } - $this->caches[$method][$type] = $cache; - return $this->caches[$method][$type]; - } - - /** - * Registers a decorator to add to all new cache objects - * @param HTMLPurifier_DefinitionCache_Decorator|string $decorator An instance or the name of a decorator - */ - public function addDecorator($decorator) - { - if (is_string($decorator)) { - $class = "HTMLPurifier_DefinitionCache_Decorator_$decorator"; - $decorator = new $class; - } - $this->decorators[$decorator->name] = $decorator; - } -} - -// vim: et sw=4 sts=4 + array()); + + /** + * @type array + */ + protected $implementations = array(); + + /** + * @type HTMLPurifier_DefinitionCache_Decorator[] + */ + protected $decorators = array(); + + /** + * Initialize default decorators + */ + public function setup() + { + $this->addDecorator('Cleanup'); + } + + /** + * Retrieves an instance of global definition cache factory. + * @param HTMLPurifier_DefinitionCacheFactory $prototype + * @return HTMLPurifier_DefinitionCacheFactory + */ + public static function instance($prototype = null) + { + static $instance; + if ($prototype !== null) { + $instance = $prototype; + } elseif ($instance === null || $prototype === true) { + $instance = new HTMLPurifier_DefinitionCacheFactory(); + $instance->setup(); + } + return $instance; + } + + /** + * Registers a new definition cache object + * @param string $short Short name of cache object, for reference + * @param string $long Full class name of cache object, for construction + */ + public function register($short, $long) + { + $this->implementations[$short] = $long; + } + + /** + * Factory method that creates a cache object based on configuration + * @param string $type Name of definitions handled by cache + * @param HTMLPurifier_Config $config Config instance + * @return mixed + */ + public function create($type, $config) + { + $method = $config->get('Cache.DefinitionImpl'); + if ($method === null) { + return new HTMLPurifier_DefinitionCache_Null($type); + } + if (!empty($this->caches[$method][$type])) { + return $this->caches[$method][$type]; + } + if (isset($this->implementations[$method]) && + class_exists($class = $this->implementations[$method], false)) { + $cache = new $class($type); + } else { + if ($method != 'Serializer') { + trigger_error("Unrecognized DefinitionCache $method, using Serializer instead", E_USER_WARNING); + } + $cache = new HTMLPurifier_DefinitionCache_Serializer($type); + } + foreach ($this->decorators as $decorator) { + $new_cache = $decorator->decorate($cache); + // prevent infinite recursion in PHP 4 + unset($cache); + $cache = $new_cache; + } + $this->caches[$method][$type] = $cache; + return $this->caches[$method][$type]; + } + + /** + * Registers a decorator to add to all new cache objects + * @param HTMLPurifier_DefinitionCache_Decorator|string $decorator An instance or the name of a decorator + */ + public function addDecorator($decorator) + { + if (is_string($decorator)) { + $class = "HTMLPurifier_DefinitionCache_Decorator_$decorator"; + $decorator = new $class; + } + $this->decorators[$decorator->name] = $decorator; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/Doctype.php b/lib/purifier/HTMLPurifier/Doctype.php index 4d723129e0..4acd06e5bd 100644 --- a/lib/purifier/HTMLPurifier/Doctype.php +++ b/lib/purifier/HTMLPurifier/Doctype.php @@ -1,73 +1,73 @@ -renderDoctype. - * If structure changes, please update that function. - */ -class HTMLPurifier_Doctype -{ - /** - * Full name of doctype - * @type string - */ - public $name; - - /** - * List of standard modules (string identifiers or literal objects) - * that this doctype uses - * @type array - */ - public $modules = array(); - - /** - * List of modules to use for tidying up code - * @type array - */ - public $tidyModules = array(); - - /** - * Is the language derived from XML (i.e. XHTML)? - * @type bool - */ - public $xml = true; - - /** - * List of aliases for this doctype - * @type array - */ - public $aliases = array(); - - /** - * Public DTD identifier - * @type string - */ - public $dtdPublic; - - /** - * System DTD identifier - * @type string - */ - public $dtdSystem; - - public function __construct( - $name = null, - $xml = true, - $modules = array(), - $tidyModules = array(), - $aliases = array(), - $dtd_public = null, - $dtd_system = null - ) { - $this->name = $name; - $this->xml = $xml; - $this->modules = $modules; - $this->tidyModules = $tidyModules; - $this->aliases = $aliases; - $this->dtdPublic = $dtd_public; - $this->dtdSystem = $dtd_system; - } -} - -// vim: et sw=4 sts=4 +renderDoctype. + * If structure changes, please update that function. + */ +class HTMLPurifier_Doctype +{ + /** + * Full name of doctype + * @type string + */ + public $name; + + /** + * List of standard modules (string identifiers or literal objects) + * that this doctype uses + * @type array + */ + public $modules = array(); + + /** + * List of modules to use for tidying up code + * @type array + */ + public $tidyModules = array(); + + /** + * Is the language derived from XML (i.e. XHTML)? + * @type bool + */ + public $xml = true; + + /** + * List of aliases for this doctype + * @type array + */ + public $aliases = array(); + + /** + * Public DTD identifier + * @type string + */ + public $dtdPublic; + + /** + * System DTD identifier + * @type string + */ + public $dtdSystem; + + public function __construct( + $name = null, + $xml = true, + $modules = array(), + $tidyModules = array(), + $aliases = array(), + $dtd_public = null, + $dtd_system = null + ) { + $this->name = $name; + $this->xml = $xml; + $this->modules = $modules; + $this->tidyModules = $tidyModules; + $this->aliases = $aliases; + $this->dtdPublic = $dtd_public; + $this->dtdSystem = $dtd_system; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/DoctypeRegistry.php b/lib/purifier/HTMLPurifier/DoctypeRegistry.php index cab9dc5361..acc1d64a62 100644 --- a/lib/purifier/HTMLPurifier/DoctypeRegistry.php +++ b/lib/purifier/HTMLPurifier/DoctypeRegistry.php @@ -1,142 +1,142 @@ -doctypes[$doctype->name] = $doctype; - $name = $doctype->name; - // hookup aliases - foreach ($doctype->aliases as $alias) { - if (isset($this->doctypes[$alias])) { - continue; - } - $this->aliases[$alias] = $name; - } - // remove old aliases - if (isset($this->aliases[$name])) { - unset($this->aliases[$name]); - } - return $doctype; - } - - /** - * Retrieves reference to a doctype of a certain name - * @note This function resolves aliases - * @note When possible, use the more fully-featured make() - * @param string $doctype Name of doctype - * @return HTMLPurifier_Doctype Editable doctype object - */ - public function get($doctype) - { - if (isset($this->aliases[$doctype])) { - $doctype = $this->aliases[$doctype]; - } - if (!isset($this->doctypes[$doctype])) { - trigger_error('Doctype ' . htmlspecialchars($doctype) . ' does not exist', E_USER_ERROR); - $anon = new HTMLPurifier_Doctype($doctype); - return $anon; - } - return $this->doctypes[$doctype]; - } - - /** - * Creates a doctype based on a configuration object, - * will perform initialization on the doctype - * @note Use this function to get a copy of doctype that config - * can hold on to (this is necessary in order to tell - * Generator whether or not the current document is XML - * based or not). - * @param HTMLPurifier_Config $config - * @return HTMLPurifier_Doctype - */ - public function make($config) - { - return clone $this->get($this->getDoctypeFromConfig($config)); - } - - /** - * Retrieves the doctype from the configuration object - * @param HTMLPurifier_Config $config - * @return string - */ - public function getDoctypeFromConfig($config) - { - // recommended test - $doctype = $config->get('HTML.Doctype'); - if (!empty($doctype)) { - return $doctype; - } - $doctype = $config->get('HTML.CustomDoctype'); - if (!empty($doctype)) { - return $doctype; - } - // backwards-compatibility - if ($config->get('HTML.XHTML')) { - $doctype = 'XHTML 1.0'; - } else { - $doctype = 'HTML 4.01'; - } - if ($config->get('HTML.Strict')) { - $doctype .= ' Strict'; - } else { - $doctype .= ' Transitional'; - } - return $doctype; - } -} - -// vim: et sw=4 sts=4 +doctypes[$doctype->name] = $doctype; + $name = $doctype->name; + // hookup aliases + foreach ($doctype->aliases as $alias) { + if (isset($this->doctypes[$alias])) { + continue; + } + $this->aliases[$alias] = $name; + } + // remove old aliases + if (isset($this->aliases[$name])) { + unset($this->aliases[$name]); + } + return $doctype; + } + + /** + * Retrieves reference to a doctype of a certain name + * @note This function resolves aliases + * @note When possible, use the more fully-featured make() + * @param string $doctype Name of doctype + * @return HTMLPurifier_Doctype Editable doctype object + */ + public function get($doctype) + { + if (isset($this->aliases[$doctype])) { + $doctype = $this->aliases[$doctype]; + } + if (!isset($this->doctypes[$doctype])) { + trigger_error('Doctype ' . htmlspecialchars($doctype) . ' does not exist', E_USER_ERROR); + $anon = new HTMLPurifier_Doctype($doctype); + return $anon; + } + return $this->doctypes[$doctype]; + } + + /** + * Creates a doctype based on a configuration object, + * will perform initialization on the doctype + * @note Use this function to get a copy of doctype that config + * can hold on to (this is necessary in order to tell + * Generator whether or not the current document is XML + * based or not). + * @param HTMLPurifier_Config $config + * @return HTMLPurifier_Doctype + */ + public function make($config) + { + return clone $this->get($this->getDoctypeFromConfig($config)); + } + + /** + * Retrieves the doctype from the configuration object + * @param HTMLPurifier_Config $config + * @return string + */ + public function getDoctypeFromConfig($config) + { + // recommended test + $doctype = $config->get('HTML.Doctype'); + if (!empty($doctype)) { + return $doctype; + } + $doctype = $config->get('HTML.CustomDoctype'); + if (!empty($doctype)) { + return $doctype; + } + // backwards-compatibility + if ($config->get('HTML.XHTML')) { + $doctype = 'XHTML 1.0'; + } else { + $doctype = 'HTML 4.01'; + } + if ($config->get('HTML.Strict')) { + $doctype .= ' Strict'; + } else { + $doctype .= ' Transitional'; + } + return $doctype; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ElementDef.php b/lib/purifier/HTMLPurifier/ElementDef.php index 089f7efe8f..d5311cedcf 100644 --- a/lib/purifier/HTMLPurifier/ElementDef.php +++ b/lib/purifier/HTMLPurifier/ElementDef.php @@ -1,216 +1,216 @@ -setup(), this array may also - * contain an array at index 0 that indicates which attribute - * collections to load into the full array. It may also - * contain string indentifiers in lieu of HTMLPurifier_AttrDef, - * see HTMLPurifier_AttrTypes on how they are expanded during - * HTMLPurifier_HTMLDefinition->setup() processing. - */ - public $attr = array(); - - // XXX: Design note: currently, it's not possible to override - // previously defined AttrTransforms without messing around with - // the final generated config. This is by design; a previous version - // used an associated list of attr_transform, but it was extremely - // easy to accidentally override other attribute transforms by - // forgetting to specify an index (and just using 0.) While we - // could check this by checking the index number and complaining, - // there is a second problem which is that it is not at all easy to - // tell when something is getting overridden. Combine this with a - // codebase where this isn't really being used, and it's perfect for - // nuking. - - /** - * List of tags HTMLPurifier_AttrTransform to be done before validation. - * @type array - */ - public $attr_transform_pre = array(); - - /** - * List of tags HTMLPurifier_AttrTransform to be done after validation. - * @type array - */ - public $attr_transform_post = array(); - - /** - * HTMLPurifier_ChildDef of this tag. - * @type HTMLPurifier_ChildDef - */ - public $child; - - /** - * Abstract string representation of internal ChildDef rules. - * @see HTMLPurifier_ContentSets for how this is parsed and then transformed - * into an HTMLPurifier_ChildDef. - * @warning This is a temporary variable that is not available after - * being processed by HTMLDefinition - * @type string - */ - public $content_model; - - /** - * Value of $child->type, used to determine which ChildDef to use, - * used in combination with $content_model. - * @warning This must be lowercase - * @warning This is a temporary variable that is not available after - * being processed by HTMLDefinition - * @type string - */ - public $content_model_type; - - /** - * Does the element have a content model (#PCDATA | Inline)*? This - * is important for chameleon ins and del processing in - * HTMLPurifier_ChildDef_Chameleon. Dynamically set: modules don't - * have to worry about this one. - * @type bool - */ - public $descendants_are_inline = false; - - /** - * List of the names of required attributes this element has. - * Dynamically populated by HTMLPurifier_HTMLDefinition::getElement() - * @type array - */ - public $required_attr = array(); - - /** - * Lookup table of tags excluded from all descendants of this tag. - * @type array - * @note SGML permits exclusions for all descendants, but this is - * not possible with DTDs or XML Schemas. W3C has elected to - * use complicated compositions of content_models to simulate - * exclusion for children, but we go the simpler, SGML-style - * route of flat-out exclusions, which correctly apply to - * all descendants and not just children. Note that the XHTML - * Modularization Abstract Modules are blithely unaware of such - * distinctions. - */ - public $excludes = array(); - - /** - * This tag is explicitly auto-closed by the following tags. - * @type array - */ - public $autoclose = array(); - - /** - * If a foreign element is found in this element, test if it is - * allowed by this sub-element; if it is, instead of closing the - * current element, place it inside this element. - * @type string - */ - public $wrap; - - /** - * Whether or not this is a formatting element affected by the - * "Active Formatting Elements" algorithm. - * @type bool - */ - public $formatting; - - /** - * Low-level factory constructor for creating new standalone element defs - */ - public static function create($content_model, $content_model_type, $attr) - { - $def = new HTMLPurifier_ElementDef(); - $def->content_model = $content_model; - $def->content_model_type = $content_model_type; - $def->attr = $attr; - return $def; - } - - /** - * Merges the values of another element definition into this one. - * Values from the new element def take precedence if a value is - * not mergeable. - * @param HTMLPurifier_ElementDef $def - */ - public function mergeIn($def) - { - // later keys takes precedence - foreach ($def->attr as $k => $v) { - if ($k === 0) { - // merge in the includes - // sorry, no way to override an include - foreach ($v as $v2) { - $this->attr[0][] = $v2; - } - continue; - } - if ($v === false) { - if (isset($this->attr[$k])) { - unset($this->attr[$k]); - } - continue; - } - $this->attr[$k] = $v; - } - $this->_mergeAssocArray($this->excludes, $def->excludes); - $this->attr_transform_pre = array_merge($this->attr_transform_pre, $def->attr_transform_pre); - $this->attr_transform_post = array_merge($this->attr_transform_post, $def->attr_transform_post); - - if (!empty($def->content_model)) { - $this->content_model = - str_replace("#SUPER", $this->content_model, $def->content_model); - $this->child = false; - } - if (!empty($def->content_model_type)) { - $this->content_model_type = $def->content_model_type; - $this->child = false; - } - if (!is_null($def->child)) { - $this->child = $def->child; - } - if (!is_null($def->formatting)) { - $this->formatting = $def->formatting; - } - if ($def->descendants_are_inline) { - $this->descendants_are_inline = $def->descendants_are_inline; - } - } - - /** - * Merges one array into another, removes values which equal false - * @param $a1 Array by reference that is merged into - * @param $a2 Array that merges into $a1 - */ - private function _mergeAssocArray(&$a1, $a2) - { - foreach ($a2 as $k => $v) { - if ($v === false) { - if (isset($a1[$k])) { - unset($a1[$k]); - } - continue; - } - $a1[$k] = $v; - } - } -} - -// vim: et sw=4 sts=4 +setup(), this array may also + * contain an array at index 0 that indicates which attribute + * collections to load into the full array. It may also + * contain string indentifiers in lieu of HTMLPurifier_AttrDef, + * see HTMLPurifier_AttrTypes on how they are expanded during + * HTMLPurifier_HTMLDefinition->setup() processing. + */ + public $attr = array(); + + // XXX: Design note: currently, it's not possible to override + // previously defined AttrTransforms without messing around with + // the final generated config. This is by design; a previous version + // used an associated list of attr_transform, but it was extremely + // easy to accidentally override other attribute transforms by + // forgetting to specify an index (and just using 0.) While we + // could check this by checking the index number and complaining, + // there is a second problem which is that it is not at all easy to + // tell when something is getting overridden. Combine this with a + // codebase where this isn't really being used, and it's perfect for + // nuking. + + /** + * List of tags HTMLPurifier_AttrTransform to be done before validation. + * @type array + */ + public $attr_transform_pre = array(); + + /** + * List of tags HTMLPurifier_AttrTransform to be done after validation. + * @type array + */ + public $attr_transform_post = array(); + + /** + * HTMLPurifier_ChildDef of this tag. + * @type HTMLPurifier_ChildDef + */ + public $child; + + /** + * Abstract string representation of internal ChildDef rules. + * @see HTMLPurifier_ContentSets for how this is parsed and then transformed + * into an HTMLPurifier_ChildDef. + * @warning This is a temporary variable that is not available after + * being processed by HTMLDefinition + * @type string + */ + public $content_model; + + /** + * Value of $child->type, used to determine which ChildDef to use, + * used in combination with $content_model. + * @warning This must be lowercase + * @warning This is a temporary variable that is not available after + * being processed by HTMLDefinition + * @type string + */ + public $content_model_type; + + /** + * Does the element have a content model (#PCDATA | Inline)*? This + * is important for chameleon ins and del processing in + * HTMLPurifier_ChildDef_Chameleon. Dynamically set: modules don't + * have to worry about this one. + * @type bool + */ + public $descendants_are_inline = false; + + /** + * List of the names of required attributes this element has. + * Dynamically populated by HTMLPurifier_HTMLDefinition::getElement() + * @type array + */ + public $required_attr = array(); + + /** + * Lookup table of tags excluded from all descendants of this tag. + * @type array + * @note SGML permits exclusions for all descendants, but this is + * not possible with DTDs or XML Schemas. W3C has elected to + * use complicated compositions of content_models to simulate + * exclusion for children, but we go the simpler, SGML-style + * route of flat-out exclusions, which correctly apply to + * all descendants and not just children. Note that the XHTML + * Modularization Abstract Modules are blithely unaware of such + * distinctions. + */ + public $excludes = array(); + + /** + * This tag is explicitly auto-closed by the following tags. + * @type array + */ + public $autoclose = array(); + + /** + * If a foreign element is found in this element, test if it is + * allowed by this sub-element; if it is, instead of closing the + * current element, place it inside this element. + * @type string + */ + public $wrap; + + /** + * Whether or not this is a formatting element affected by the + * "Active Formatting Elements" algorithm. + * @type bool + */ + public $formatting; + + /** + * Low-level factory constructor for creating new standalone element defs + */ + public static function create($content_model, $content_model_type, $attr) + { + $def = new HTMLPurifier_ElementDef(); + $def->content_model = $content_model; + $def->content_model_type = $content_model_type; + $def->attr = $attr; + return $def; + } + + /** + * Merges the values of another element definition into this one. + * Values from the new element def take precedence if a value is + * not mergeable. + * @param HTMLPurifier_ElementDef $def + */ + public function mergeIn($def) + { + // later keys takes precedence + foreach ($def->attr as $k => $v) { + if ($k === 0) { + // merge in the includes + // sorry, no way to override an include + foreach ($v as $v2) { + $this->attr[0][] = $v2; + } + continue; + } + if ($v === false) { + if (isset($this->attr[$k])) { + unset($this->attr[$k]); + } + continue; + } + $this->attr[$k] = $v; + } + $this->_mergeAssocArray($this->excludes, $def->excludes); + $this->attr_transform_pre = array_merge($this->attr_transform_pre, $def->attr_transform_pre); + $this->attr_transform_post = array_merge($this->attr_transform_post, $def->attr_transform_post); + + if (!empty($def->content_model)) { + $this->content_model = + str_replace("#SUPER", $this->content_model, $def->content_model); + $this->child = false; + } + if (!empty($def->content_model_type)) { + $this->content_model_type = $def->content_model_type; + $this->child = false; + } + if (!is_null($def->child)) { + $this->child = $def->child; + } + if (!is_null($def->formatting)) { + $this->formatting = $def->formatting; + } + if ($def->descendants_are_inline) { + $this->descendants_are_inline = $def->descendants_are_inline; + } + } + + /** + * Merges one array into another, removes values which equal false + * @param $a1 Array by reference that is merged into + * @param $a2 Array that merges into $a1 + */ + private function _mergeAssocArray(&$a1, $a2) + { + foreach ($a2 as $k => $v) { + if ($v === false) { + if (isset($a1[$k])) { + unset($a1[$k]); + } + continue; + } + $a1[$k] = $v; + } + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/Encoder.php b/lib/purifier/HTMLPurifier/Encoder.php index ea13abcdbc..40a24266a4 100644 --- a/lib/purifier/HTMLPurifier/Encoder.php +++ b/lib/purifier/HTMLPurifier/Encoder.php @@ -1,617 +1,617 @@ -= $c) { - $r .= self::unsafeIconv($in, $out, substr($text, $i)); - break; - } - // wibble the boundary - if (0x80 != (0xC0 & ord($text[$i + $max_chunk_size]))) { - $chunk_size = $max_chunk_size; - } elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 1]))) { - $chunk_size = $max_chunk_size - 1; - } elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 2]))) { - $chunk_size = $max_chunk_size - 2; - } elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 3]))) { - $chunk_size = $max_chunk_size - 3; - } else { - return false; // rather confusing UTF-8... - } - $chunk = substr($text, $i, $chunk_size); // substr doesn't mind overlong lengths - $r .= self::unsafeIconv($in, $out, $chunk); - $i += $chunk_size; - } - return $r; - } else { - return false; - } - } else { - return false; - } - } - - /** - * Cleans a UTF-8 string for well-formedness and SGML validity - * - * It will parse according to UTF-8 and return a valid UTF8 string, with - * non-SGML codepoints excluded. - * - * Specifically, it will permit: - * \x{9}\x{A}\x{D}\x{20}-\x{7E}\x{A0}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF} - * Source: https://www.w3.org/TR/REC-xml/#NT-Char - * Arguably this function should be modernized to the HTML5 set - * of allowed characters: - * https://www.w3.org/TR/html5/syntax.html#preprocessing-the-input-stream - * which simultaneously expand and restrict the set of allowed characters. - * - * @param string $str The string to clean - * @param bool $force_php - * @return string - * - * @note Just for reference, the non-SGML code points are 0 to 31 and - * 127 to 159, inclusive. However, we allow code points 9, 10 - * and 13, which are the tab, line feed and carriage return - * respectively. 128 and above the code points map to multibyte - * UTF-8 representations. - * - * @note Fallback code adapted from utf8ToUnicode by Henri Sivonen and - * hsivonen@iki.fi at under the - * LGPL license. Notes on what changed are inside, but in general, - * the original code transformed UTF-8 text into an array of integer - * Unicode codepoints. Understandably, transforming that back to - * a string would be somewhat expensive, so the function was modded to - * directly operate on the string. However, this discourages code - * reuse, and the logic enumerated here would be useful for any - * function that needs to be able to understand UTF-8 characters. - * As of right now, only smart lossless character encoding converters - * would need that, and I'm probably not going to implement them. - */ - public static function cleanUTF8($str, $force_php = false) - { - // UTF-8 validity is checked since PHP 4.3.5 - // This is an optimization: if the string is already valid UTF-8, no - // need to do PHP stuff. 99% of the time, this will be the case. - if (preg_match( - '/^[\x{9}\x{A}\x{D}\x{20}-\x{7E}\x{A0}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]*$/Du', - $str - )) { - return $str; - } - - $mState = 0; // cached expected number of octets after the current octet - // until the beginning of the next UTF8 character sequence - $mUcs4 = 0; // cached Unicode character - $mBytes = 1; // cached expected number of octets in the current sequence - - // original code involved an $out that was an array of Unicode - // codepoints. Instead of having to convert back into UTF-8, we've - // decided to directly append valid UTF-8 characters onto a string - // $out once they're done. $char accumulates raw bytes, while $mUcs4 - // turns into the Unicode code point, so there's some redundancy. - - $out = ''; - $char = ''; - - $len = strlen($str); - for ($i = 0; $i < $len; $i++) { - $in = ord($str{$i}); - $char .= $str[$i]; // append byte to char - if (0 == $mState) { - // When mState is zero we expect either a US-ASCII character - // or a multi-octet sequence. - if (0 == (0x80 & ($in))) { - // US-ASCII, pass straight through. - if (($in <= 31 || $in == 127) && - !($in == 9 || $in == 13 || $in == 10) // save \r\t\n - ) { - // control characters, remove - } else { - $out .= $char; - } - // reset - $char = ''; - $mBytes = 1; - } elseif (0xC0 == (0xE0 & ($in))) { - // First octet of 2 octet sequence - $mUcs4 = ($in); - $mUcs4 = ($mUcs4 & 0x1F) << 6; - $mState = 1; - $mBytes = 2; - } elseif (0xE0 == (0xF0 & ($in))) { - // First octet of 3 octet sequence - $mUcs4 = ($in); - $mUcs4 = ($mUcs4 & 0x0F) << 12; - $mState = 2; - $mBytes = 3; - } elseif (0xF0 == (0xF8 & ($in))) { - // First octet of 4 octet sequence - $mUcs4 = ($in); - $mUcs4 = ($mUcs4 & 0x07) << 18; - $mState = 3; - $mBytes = 4; - } elseif (0xF8 == (0xFC & ($in))) { - // First octet of 5 octet sequence. - // - // This is illegal because the encoded codepoint must be - // either: - // (a) not the shortest form or - // (b) outside the Unicode range of 0-0x10FFFF. - // Rather than trying to resynchronize, we will carry on - // until the end of the sequence and let the later error - // handling code catch it. - $mUcs4 = ($in); - $mUcs4 = ($mUcs4 & 0x03) << 24; - $mState = 4; - $mBytes = 5; - } elseif (0xFC == (0xFE & ($in))) { - // First octet of 6 octet sequence, see comments for 5 - // octet sequence. - $mUcs4 = ($in); - $mUcs4 = ($mUcs4 & 1) << 30; - $mState = 5; - $mBytes = 6; - } else { - // Current octet is neither in the US-ASCII range nor a - // legal first octet of a multi-octet sequence. - $mState = 0; - $mUcs4 = 0; - $mBytes = 1; - $char = ''; - } - } else { - // When mState is non-zero, we expect a continuation of the - // multi-octet sequence - if (0x80 == (0xC0 & ($in))) { - // Legal continuation. - $shift = ($mState - 1) * 6; - $tmp = $in; - $tmp = ($tmp & 0x0000003F) << $shift; - $mUcs4 |= $tmp; - - if (0 == --$mState) { - // End of the multi-octet sequence. mUcs4 now contains - // the final Unicode codepoint to be output - - // Check for illegal sequences and codepoints. - - // From Unicode 3.1, non-shortest form is illegal - if (((2 == $mBytes) && ($mUcs4 < 0x0080)) || - ((3 == $mBytes) && ($mUcs4 < 0x0800)) || - ((4 == $mBytes) && ($mUcs4 < 0x10000)) || - (4 < $mBytes) || - // From Unicode 3.2, surrogate characters = illegal - (($mUcs4 & 0xFFFFF800) == 0xD800) || - // Codepoints outside the Unicode range are illegal - ($mUcs4 > 0x10FFFF) - ) { - - } elseif (0xFEFF != $mUcs4 && // omit BOM - // check for valid Char unicode codepoints - ( - 0x9 == $mUcs4 || - 0xA == $mUcs4 || - 0xD == $mUcs4 || - (0x20 <= $mUcs4 && 0x7E >= $mUcs4) || - // 7F-9F is not strictly prohibited by XML, - // but it is non-SGML, and thus we don't allow it - (0xA0 <= $mUcs4 && 0xD7FF >= $mUcs4) || - (0xE000 <= $mUcs4 && 0xFFFD >= $mUcs4) || - (0x10000 <= $mUcs4 && 0x10FFFF >= $mUcs4) - ) - ) { - $out .= $char; - } - // initialize UTF8 cache (reset) - $mState = 0; - $mUcs4 = 0; - $mBytes = 1; - $char = ''; - } - } else { - // ((0xC0 & (*in) != 0x80) && (mState != 0)) - // Incomplete multi-octet sequence. - // used to result in complete fail, but we'll reset - $mState = 0; - $mUcs4 = 0; - $mBytes = 1; - $char =''; - } - } - } - return $out; - } - - /** - * Translates a Unicode codepoint into its corresponding UTF-8 character. - * @note Based on Feyd's function at - * , - * which is in public domain. - * @note While we're going to do code point parsing anyway, a good - * optimization would be to refuse to translate code points that - * are non-SGML characters. However, this could lead to duplication. - * @note This is very similar to the unichr function in - * maintenance/generate-entity-file.php (although this is superior, - * due to its sanity checks). - */ - - // +----------+----------+----------+----------+ - // | 33222222 | 22221111 | 111111 | | - // | 10987654 | 32109876 | 54321098 | 76543210 | bit - // +----------+----------+----------+----------+ - // | | | | 0xxxxxxx | 1 byte 0x00000000..0x0000007F - // | | | 110yyyyy | 10xxxxxx | 2 byte 0x00000080..0x000007FF - // | | 1110zzzz | 10yyyyyy | 10xxxxxx | 3 byte 0x00000800..0x0000FFFF - // | 11110www | 10wwzzzz | 10yyyyyy | 10xxxxxx | 4 byte 0x00010000..0x0010FFFF - // +----------+----------+----------+----------+ - // | 00000000 | 00011111 | 11111111 | 11111111 | Theoretical upper limit of legal scalars: 2097151 (0x001FFFFF) - // | 00000000 | 00010000 | 11111111 | 11111111 | Defined upper limit of legal scalar codes - // +----------+----------+----------+----------+ - - public static function unichr($code) - { - if ($code > 1114111 or $code < 0 or - ($code >= 55296 and $code <= 57343) ) { - // bits are set outside the "valid" range as defined - // by UNICODE 4.1.0 - return ''; - } - - $x = $y = $z = $w = 0; - if ($code < 128) { - // regular ASCII character - $x = $code; - } else { - // set up bits for UTF-8 - $x = ($code & 63) | 128; - if ($code < 2048) { - $y = (($code & 2047) >> 6) | 192; - } else { - $y = (($code & 4032) >> 6) | 128; - if ($code < 65536) { - $z = (($code >> 12) & 15) | 224; - } else { - $z = (($code >> 12) & 63) | 128; - $w = (($code >> 18) & 7) | 240; - } - } - } - // set up the actual character - $ret = ''; - if ($w) { - $ret .= chr($w); - } - if ($z) { - $ret .= chr($z); - } - if ($y) { - $ret .= chr($y); - } - $ret .= chr($x); - - return $ret; - } - - /** - * @return bool - */ - public static function iconvAvailable() - { - static $iconv = null; - if ($iconv === null) { - $iconv = function_exists('iconv') && self::testIconvTruncateBug() != self::ICONV_UNUSABLE; - } - return $iconv; - } - - /** - * Convert a string to UTF-8 based on configuration. - * @param string $str The string to convert - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return string - */ - public static function convertToUTF8($str, $config, $context) - { - $encoding = $config->get('Core.Encoding'); - if ($encoding === 'utf-8') { - return $str; - } - static $iconv = null; - if ($iconv === null) { - $iconv = self::iconvAvailable(); - } - if ($iconv && !$config->get('Test.ForceNoIconv')) { - // unaffected by bugs, since UTF-8 support all characters - $str = self::unsafeIconv($encoding, 'utf-8//IGNORE', $str); - if ($str === false) { - // $encoding is not a valid encoding - trigger_error('Invalid encoding ' . $encoding, E_USER_ERROR); - return ''; - } - // If the string is bjorked by Shift_JIS or a similar encoding - // that doesn't support all of ASCII, convert the naughty - // characters to their true byte-wise ASCII/UTF-8 equivalents. - $str = strtr($str, self::testEncodingSupportsASCII($encoding)); - return $str; - } elseif ($encoding === 'iso-8859-1') { - $str = utf8_encode($str); - return $str; - } - $bug = HTMLPurifier_Encoder::testIconvTruncateBug(); - if ($bug == self::ICONV_OK) { - trigger_error('Encoding not supported, please install iconv', E_USER_ERROR); - } else { - trigger_error( - 'You have a buggy version of iconv, see https://bugs.php.net/bug.php?id=48147 ' . - 'and http://sourceware.org/bugzilla/show_bug.cgi?id=13541', - E_USER_ERROR - ); - } - } - - /** - * Converts a string from UTF-8 based on configuration. - * @param string $str The string to convert - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return string - * @note Currently, this is a lossy conversion, with unexpressable - * characters being omitted. - */ - public static function convertFromUTF8($str, $config, $context) - { - $encoding = $config->get('Core.Encoding'); - if ($escape = $config->get('Core.EscapeNonASCIICharacters')) { - $str = self::convertToASCIIDumbLossless($str); - } - if ($encoding === 'utf-8') { - return $str; - } - static $iconv = null; - if ($iconv === null) { - $iconv = self::iconvAvailable(); - } - if ($iconv && !$config->get('Test.ForceNoIconv')) { - // Undo our previous fix in convertToUTF8, otherwise iconv will barf - $ascii_fix = self::testEncodingSupportsASCII($encoding); - if (!$escape && !empty($ascii_fix)) { - $clear_fix = array(); - foreach ($ascii_fix as $utf8 => $native) { - $clear_fix[$utf8] = ''; - } - $str = strtr($str, $clear_fix); - } - $str = strtr($str, array_flip($ascii_fix)); - // Normal stuff - $str = self::iconv('utf-8', $encoding . '//IGNORE', $str); - return $str; - } elseif ($encoding === 'iso-8859-1') { - $str = utf8_decode($str); - return $str; - } - trigger_error('Encoding not supported', E_USER_ERROR); - // You might be tempted to assume that the ASCII representation - // might be OK, however, this is *not* universally true over all - // encodings. So we take the conservative route here, rather - // than forcibly turn on %Core.EscapeNonASCIICharacters - } - - /** - * Lossless (character-wise) conversion of HTML to ASCII - * @param string $str UTF-8 string to be converted to ASCII - * @return string ASCII encoded string with non-ASCII character entity-ized - * @warning Adapted from MediaWiki, claiming fair use: this is a common - * algorithm. If you disagree with this license fudgery, - * implement it yourself. - * @note Uses decimal numeric entities since they are best supported. - * @note This is a DUMB function: it has no concept of keeping - * character entities that the projected character encoding - * can allow. We could possibly implement a smart version - * but that would require it to also know which Unicode - * codepoints the charset supported (not an easy task). - * @note Sort of with cleanUTF8() but it assumes that $str is - * well-formed UTF-8 - */ - public static function convertToASCIIDumbLossless($str) - { - $bytesleft = 0; - $result = ''; - $working = 0; - $len = strlen($str); - for ($i = 0; $i < $len; $i++) { - $bytevalue = ord($str[$i]); - if ($bytevalue <= 0x7F) { //0xxx xxxx - $result .= chr($bytevalue); - $bytesleft = 0; - } elseif ($bytevalue <= 0xBF) { //10xx xxxx - $working = $working << 6; - $working += ($bytevalue & 0x3F); - $bytesleft--; - if ($bytesleft <= 0) { - $result .= "&#" . $working . ";"; - } - } elseif ($bytevalue <= 0xDF) { //110x xxxx - $working = $bytevalue & 0x1F; - $bytesleft = 1; - } elseif ($bytevalue <= 0xEF) { //1110 xxxx - $working = $bytevalue & 0x0F; - $bytesleft = 2; - } else { //1111 0xxx - $working = $bytevalue & 0x07; - $bytesleft = 3; - } - } - return $result; - } - - /** No bugs detected in iconv. */ - const ICONV_OK = 0; - - /** Iconv truncates output if converting from UTF-8 to another - * character set with //IGNORE, and a non-encodable character is found */ - const ICONV_TRUNCATES = 1; - - /** Iconv does not support //IGNORE, making it unusable for - * transcoding purposes */ - const ICONV_UNUSABLE = 2; - - /** - * glibc iconv has a known bug where it doesn't handle the magic - * //IGNORE stanza correctly. In particular, rather than ignore - * characters, it will return an EILSEQ after consuming some number - * of characters, and expect you to restart iconv as if it were - * an E2BIG. Old versions of PHP did not respect the errno, and - * returned the fragment, so as a result you would see iconv - * mysteriously truncating output. We can work around this by - * manually chopping our input into segments of about 8000 - * characters, as long as PHP ignores the error code. If PHP starts - * paying attention to the error code, iconv becomes unusable. - * - * @return int Error code indicating severity of bug. - */ - public static function testIconvTruncateBug() - { - static $code = null; - if ($code === null) { - // better not use iconv, otherwise infinite loop! - $r = self::unsafeIconv('utf-8', 'ascii//IGNORE', "\xCE\xB1" . str_repeat('a', 9000)); - if ($r === false) { - $code = self::ICONV_UNUSABLE; - } elseif (($c = strlen($r)) < 9000) { - $code = self::ICONV_TRUNCATES; - } elseif ($c > 9000) { - trigger_error( - 'Your copy of iconv is extremely buggy. Please notify HTML Purifier maintainers: ' . - 'include your iconv version as per phpversion()', - E_USER_ERROR - ); - } else { - $code = self::ICONV_OK; - } - } - return $code; - } - - /** - * This expensive function tests whether or not a given character - * encoding supports ASCII. 7/8-bit encodings like Shift_JIS will - * fail this test, and require special processing. Variable width - * encodings shouldn't ever fail. - * - * @param string $encoding Encoding name to test, as per iconv format - * @param bool $bypass Whether or not to bypass the precompiled arrays. - * @return Array of UTF-8 characters to their corresponding ASCII, - * which can be used to "undo" any overzealous iconv action. - */ - public static function testEncodingSupportsASCII($encoding, $bypass = false) - { - // All calls to iconv here are unsafe, proof by case analysis: - // If ICONV_OK, no difference. - // If ICONV_TRUNCATE, all calls involve one character inputs, - // so bug is not triggered. - // If ICONV_UNUSABLE, this call is irrelevant - static $encodings = array(); - if (!$bypass) { - if (isset($encodings[$encoding])) { - return $encodings[$encoding]; - } - $lenc = strtolower($encoding); - switch ($lenc) { - case 'shift_jis': - return array("\xC2\xA5" => '\\', "\xE2\x80\xBE" => '~'); - case 'johab': - return array("\xE2\x82\xA9" => '\\'); - } - if (strpos($lenc, 'iso-8859-') === 0) { - return array(); - } - } - $ret = array(); - if (self::unsafeIconv('UTF-8', $encoding, 'a') === false) { - return false; - } - for ($i = 0x20; $i <= 0x7E; $i++) { // all printable ASCII chars - $c = chr($i); // UTF-8 char - $r = self::unsafeIconv('UTF-8', "$encoding//IGNORE", $c); // initial conversion - if ($r === '' || - // This line is needed for iconv implementations that do not - // omit characters that do not exist in the target character set - ($r === $c && self::unsafeIconv($encoding, 'UTF-8//IGNORE', $r) !== $c) - ) { - // Reverse engineer: what's the UTF-8 equiv of this byte - // sequence? This assumes that there's no variable width - // encoding that doesn't support ASCII. - $ret[self::unsafeIconv($encoding, 'UTF-8//IGNORE', $c)] = $c; - } - } - $encodings[$encoding] = $ret; - return $ret; - } -} - -// vim: et sw=4 sts=4 += $c) { + $r .= self::unsafeIconv($in, $out, substr($text, $i)); + break; + } + // wibble the boundary + if (0x80 != (0xC0 & ord($text[$i + $max_chunk_size]))) { + $chunk_size = $max_chunk_size; + } elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 1]))) { + $chunk_size = $max_chunk_size - 1; + } elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 2]))) { + $chunk_size = $max_chunk_size - 2; + } elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 3]))) { + $chunk_size = $max_chunk_size - 3; + } else { + return false; // rather confusing UTF-8... + } + $chunk = substr($text, $i, $chunk_size); // substr doesn't mind overlong lengths + $r .= self::unsafeIconv($in, $out, $chunk); + $i += $chunk_size; + } + return $r; + } else { + return false; + } + } else { + return false; + } + } + + /** + * Cleans a UTF-8 string for well-formedness and SGML validity + * + * It will parse according to UTF-8 and return a valid UTF8 string, with + * non-SGML codepoints excluded. + * + * Specifically, it will permit: + * \x{9}\x{A}\x{D}\x{20}-\x{7E}\x{A0}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF} + * Source: https://www.w3.org/TR/REC-xml/#NT-Char + * Arguably this function should be modernized to the HTML5 set + * of allowed characters: + * https://www.w3.org/TR/html5/syntax.html#preprocessing-the-input-stream + * which simultaneously expand and restrict the set of allowed characters. + * + * @param string $str The string to clean + * @param bool $force_php + * @return string + * + * @note Just for reference, the non-SGML code points are 0 to 31 and + * 127 to 159, inclusive. However, we allow code points 9, 10 + * and 13, which are the tab, line feed and carriage return + * respectively. 128 and above the code points map to multibyte + * UTF-8 representations. + * + * @note Fallback code adapted from utf8ToUnicode by Henri Sivonen and + * hsivonen@iki.fi at under the + * LGPL license. Notes on what changed are inside, but in general, + * the original code transformed UTF-8 text into an array of integer + * Unicode codepoints. Understandably, transforming that back to + * a string would be somewhat expensive, so the function was modded to + * directly operate on the string. However, this discourages code + * reuse, and the logic enumerated here would be useful for any + * function that needs to be able to understand UTF-8 characters. + * As of right now, only smart lossless character encoding converters + * would need that, and I'm probably not going to implement them. + */ + public static function cleanUTF8($str, $force_php = false) + { + // UTF-8 validity is checked since PHP 4.3.5 + // This is an optimization: if the string is already valid UTF-8, no + // need to do PHP stuff. 99% of the time, this will be the case. + if (preg_match( + '/^[\x{9}\x{A}\x{D}\x{20}-\x{7E}\x{A0}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]*$/Du', + $str + )) { + return $str; + } + + $mState = 0; // cached expected number of octets after the current octet + // until the beginning of the next UTF8 character sequence + $mUcs4 = 0; // cached Unicode character + $mBytes = 1; // cached expected number of octets in the current sequence + + // original code involved an $out that was an array of Unicode + // codepoints. Instead of having to convert back into UTF-8, we've + // decided to directly append valid UTF-8 characters onto a string + // $out once they're done. $char accumulates raw bytes, while $mUcs4 + // turns into the Unicode code point, so there's some redundancy. + + $out = ''; + $char = ''; + + $len = strlen($str); + for ($i = 0; $i < $len; $i++) { + $in = ord($str[$i]); + $char .= $str[$i]; // append byte to char + if (0 == $mState) { + // When mState is zero we expect either a US-ASCII character + // or a multi-octet sequence. + if (0 == (0x80 & ($in))) { + // US-ASCII, pass straight through. + if (($in <= 31 || $in == 127) && + !($in == 9 || $in == 13 || $in == 10) // save \r\t\n + ) { + // control characters, remove + } else { + $out .= $char; + } + // reset + $char = ''; + $mBytes = 1; + } elseif (0xC0 == (0xE0 & ($in))) { + // First octet of 2 octet sequence + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x1F) << 6; + $mState = 1; + $mBytes = 2; + } elseif (0xE0 == (0xF0 & ($in))) { + // First octet of 3 octet sequence + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x0F) << 12; + $mState = 2; + $mBytes = 3; + } elseif (0xF0 == (0xF8 & ($in))) { + // First octet of 4 octet sequence + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x07) << 18; + $mState = 3; + $mBytes = 4; + } elseif (0xF8 == (0xFC & ($in))) { + // First octet of 5 octet sequence. + // + // This is illegal because the encoded codepoint must be + // either: + // (a) not the shortest form or + // (b) outside the Unicode range of 0-0x10FFFF. + // Rather than trying to resynchronize, we will carry on + // until the end of the sequence and let the later error + // handling code catch it. + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 0x03) << 24; + $mState = 4; + $mBytes = 5; + } elseif (0xFC == (0xFE & ($in))) { + // First octet of 6 octet sequence, see comments for 5 + // octet sequence. + $mUcs4 = ($in); + $mUcs4 = ($mUcs4 & 1) << 30; + $mState = 5; + $mBytes = 6; + } else { + // Current octet is neither in the US-ASCII range nor a + // legal first octet of a multi-octet sequence. + $mState = 0; + $mUcs4 = 0; + $mBytes = 1; + $char = ''; + } + } else { + // When mState is non-zero, we expect a continuation of the + // multi-octet sequence + if (0x80 == (0xC0 & ($in))) { + // Legal continuation. + $shift = ($mState - 1) * 6; + $tmp = $in; + $tmp = ($tmp & 0x0000003F) << $shift; + $mUcs4 |= $tmp; + + if (0 == --$mState) { + // End of the multi-octet sequence. mUcs4 now contains + // the final Unicode codepoint to be output + + // Check for illegal sequences and codepoints. + + // From Unicode 3.1, non-shortest form is illegal + if (((2 == $mBytes) && ($mUcs4 < 0x0080)) || + ((3 == $mBytes) && ($mUcs4 < 0x0800)) || + ((4 == $mBytes) && ($mUcs4 < 0x10000)) || + (4 < $mBytes) || + // From Unicode 3.2, surrogate characters = illegal + (($mUcs4 & 0xFFFFF800) == 0xD800) || + // Codepoints outside the Unicode range are illegal + ($mUcs4 > 0x10FFFF) + ) { + + } elseif (0xFEFF != $mUcs4 && // omit BOM + // check for valid Char unicode codepoints + ( + 0x9 == $mUcs4 || + 0xA == $mUcs4 || + 0xD == $mUcs4 || + (0x20 <= $mUcs4 && 0x7E >= $mUcs4) || + // 7F-9F is not strictly prohibited by XML, + // but it is non-SGML, and thus we don't allow it + (0xA0 <= $mUcs4 && 0xD7FF >= $mUcs4) || + (0xE000 <= $mUcs4 && 0xFFFD >= $mUcs4) || + (0x10000 <= $mUcs4 && 0x10FFFF >= $mUcs4) + ) + ) { + $out .= $char; + } + // initialize UTF8 cache (reset) + $mState = 0; + $mUcs4 = 0; + $mBytes = 1; + $char = ''; + } + } else { + // ((0xC0 & (*in) != 0x80) && (mState != 0)) + // Incomplete multi-octet sequence. + // used to result in complete fail, but we'll reset + $mState = 0; + $mUcs4 = 0; + $mBytes = 1; + $char =''; + } + } + } + return $out; + } + + /** + * Translates a Unicode codepoint into its corresponding UTF-8 character. + * @note Based on Feyd's function at + * , + * which is in public domain. + * @note While we're going to do code point parsing anyway, a good + * optimization would be to refuse to translate code points that + * are non-SGML characters. However, this could lead to duplication. + * @note This is very similar to the unichr function in + * maintenance/generate-entity-file.php (although this is superior, + * due to its sanity checks). + */ + + // +----------+----------+----------+----------+ + // | 33222222 | 22221111 | 111111 | | + // | 10987654 | 32109876 | 54321098 | 76543210 | bit + // +----------+----------+----------+----------+ + // | | | | 0xxxxxxx | 1 byte 0x00000000..0x0000007F + // | | | 110yyyyy | 10xxxxxx | 2 byte 0x00000080..0x000007FF + // | | 1110zzzz | 10yyyyyy | 10xxxxxx | 3 byte 0x00000800..0x0000FFFF + // | 11110www | 10wwzzzz | 10yyyyyy | 10xxxxxx | 4 byte 0x00010000..0x0010FFFF + // +----------+----------+----------+----------+ + // | 00000000 | 00011111 | 11111111 | 11111111 | Theoretical upper limit of legal scalars: 2097151 (0x001FFFFF) + // | 00000000 | 00010000 | 11111111 | 11111111 | Defined upper limit of legal scalar codes + // +----------+----------+----------+----------+ + + public static function unichr($code) + { + if ($code > 1114111 or $code < 0 or + ($code >= 55296 and $code <= 57343) ) { + // bits are set outside the "valid" range as defined + // by UNICODE 4.1.0 + return ''; + } + + $x = $y = $z = $w = 0; + if ($code < 128) { + // regular ASCII character + $x = $code; + } else { + // set up bits for UTF-8 + $x = ($code & 63) | 128; + if ($code < 2048) { + $y = (($code & 2047) >> 6) | 192; + } else { + $y = (($code & 4032) >> 6) | 128; + if ($code < 65536) { + $z = (($code >> 12) & 15) | 224; + } else { + $z = (($code >> 12) & 63) | 128; + $w = (($code >> 18) & 7) | 240; + } + } + } + // set up the actual character + $ret = ''; + if ($w) { + $ret .= chr($w); + } + if ($z) { + $ret .= chr($z); + } + if ($y) { + $ret .= chr($y); + } + $ret .= chr($x); + + return $ret; + } + + /** + * @return bool + */ + public static function iconvAvailable() + { + static $iconv = null; + if ($iconv === null) { + $iconv = function_exists('iconv') && self::testIconvTruncateBug() != self::ICONV_UNUSABLE; + } + return $iconv; + } + + /** + * Convert a string to UTF-8 based on configuration. + * @param string $str The string to convert + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + */ + public static function convertToUTF8($str, $config, $context) + { + $encoding = $config->get('Core.Encoding'); + if ($encoding === 'utf-8') { + return $str; + } + static $iconv = null; + if ($iconv === null) { + $iconv = self::iconvAvailable(); + } + if ($iconv && !$config->get('Test.ForceNoIconv')) { + // unaffected by bugs, since UTF-8 support all characters + $str = self::unsafeIconv($encoding, 'utf-8//IGNORE', $str); + if ($str === false) { + // $encoding is not a valid encoding + trigger_error('Invalid encoding ' . $encoding, E_USER_ERROR); + return ''; + } + // If the string is bjorked by Shift_JIS or a similar encoding + // that doesn't support all of ASCII, convert the naughty + // characters to their true byte-wise ASCII/UTF-8 equivalents. + $str = strtr($str, self::testEncodingSupportsASCII($encoding)); + return $str; + } elseif ($encoding === 'iso-8859-1') { + $str = utf8_encode($str); + return $str; + } + $bug = HTMLPurifier_Encoder::testIconvTruncateBug(); + if ($bug == self::ICONV_OK) { + trigger_error('Encoding not supported, please install iconv', E_USER_ERROR); + } else { + trigger_error( + 'You have a buggy version of iconv, see https://bugs.php.net/bug.php?id=48147 ' . + 'and http://sourceware.org/bugzilla/show_bug.cgi?id=13541', + E_USER_ERROR + ); + } + } + + /** + * Converts a string from UTF-8 based on configuration. + * @param string $str The string to convert + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + * @note Currently, this is a lossy conversion, with unexpressable + * characters being omitted. + */ + public static function convertFromUTF8($str, $config, $context) + { + $encoding = $config->get('Core.Encoding'); + if ($escape = $config->get('Core.EscapeNonASCIICharacters')) { + $str = self::convertToASCIIDumbLossless($str); + } + if ($encoding === 'utf-8') { + return $str; + } + static $iconv = null; + if ($iconv === null) { + $iconv = self::iconvAvailable(); + } + if ($iconv && !$config->get('Test.ForceNoIconv')) { + // Undo our previous fix in convertToUTF8, otherwise iconv will barf + $ascii_fix = self::testEncodingSupportsASCII($encoding); + if (!$escape && !empty($ascii_fix)) { + $clear_fix = array(); + foreach ($ascii_fix as $utf8 => $native) { + $clear_fix[$utf8] = ''; + } + $str = strtr($str, $clear_fix); + } + $str = strtr($str, array_flip($ascii_fix)); + // Normal stuff + $str = self::iconv('utf-8', $encoding . '//IGNORE', $str); + return $str; + } elseif ($encoding === 'iso-8859-1') { + $str = utf8_decode($str); + return $str; + } + trigger_error('Encoding not supported', E_USER_ERROR); + // You might be tempted to assume that the ASCII representation + // might be OK, however, this is *not* universally true over all + // encodings. So we take the conservative route here, rather + // than forcibly turn on %Core.EscapeNonASCIICharacters + } + + /** + * Lossless (character-wise) conversion of HTML to ASCII + * @param string $str UTF-8 string to be converted to ASCII + * @return string ASCII encoded string with non-ASCII character entity-ized + * @warning Adapted from MediaWiki, claiming fair use: this is a common + * algorithm. If you disagree with this license fudgery, + * implement it yourself. + * @note Uses decimal numeric entities since they are best supported. + * @note This is a DUMB function: it has no concept of keeping + * character entities that the projected character encoding + * can allow. We could possibly implement a smart version + * but that would require it to also know which Unicode + * codepoints the charset supported (not an easy task). + * @note Sort of with cleanUTF8() but it assumes that $str is + * well-formed UTF-8 + */ + public static function convertToASCIIDumbLossless($str) + { + $bytesleft = 0; + $result = ''; + $working = 0; + $len = strlen($str); + for ($i = 0; $i < $len; $i++) { + $bytevalue = ord($str[$i]); + if ($bytevalue <= 0x7F) { //0xxx xxxx + $result .= chr($bytevalue); + $bytesleft = 0; + } elseif ($bytevalue <= 0xBF) { //10xx xxxx + $working = $working << 6; + $working += ($bytevalue & 0x3F); + $bytesleft--; + if ($bytesleft <= 0) { + $result .= "&#" . $working . ";"; + } + } elseif ($bytevalue <= 0xDF) { //110x xxxx + $working = $bytevalue & 0x1F; + $bytesleft = 1; + } elseif ($bytevalue <= 0xEF) { //1110 xxxx + $working = $bytevalue & 0x0F; + $bytesleft = 2; + } else { //1111 0xxx + $working = $bytevalue & 0x07; + $bytesleft = 3; + } + } + return $result; + } + + /** No bugs detected in iconv. */ + const ICONV_OK = 0; + + /** Iconv truncates output if converting from UTF-8 to another + * character set with //IGNORE, and a non-encodable character is found */ + const ICONV_TRUNCATES = 1; + + /** Iconv does not support //IGNORE, making it unusable for + * transcoding purposes */ + const ICONV_UNUSABLE = 2; + + /** + * glibc iconv has a known bug where it doesn't handle the magic + * //IGNORE stanza correctly. In particular, rather than ignore + * characters, it will return an EILSEQ after consuming some number + * of characters, and expect you to restart iconv as if it were + * an E2BIG. Old versions of PHP did not respect the errno, and + * returned the fragment, so as a result you would see iconv + * mysteriously truncating output. We can work around this by + * manually chopping our input into segments of about 8000 + * characters, as long as PHP ignores the error code. If PHP starts + * paying attention to the error code, iconv becomes unusable. + * + * @return int Error code indicating severity of bug. + */ + public static function testIconvTruncateBug() + { + static $code = null; + if ($code === null) { + // better not use iconv, otherwise infinite loop! + $r = self::unsafeIconv('utf-8', 'ascii//IGNORE', "\xCE\xB1" . str_repeat('a', 9000)); + if ($r === false) { + $code = self::ICONV_UNUSABLE; + } elseif (($c = strlen($r)) < 9000) { + $code = self::ICONV_TRUNCATES; + } elseif ($c > 9000) { + trigger_error( + 'Your copy of iconv is extremely buggy. Please notify HTML Purifier maintainers: ' . + 'include your iconv version as per phpversion()', + E_USER_ERROR + ); + } else { + $code = self::ICONV_OK; + } + } + return $code; + } + + /** + * This expensive function tests whether or not a given character + * encoding supports ASCII. 7/8-bit encodings like Shift_JIS will + * fail this test, and require special processing. Variable width + * encodings shouldn't ever fail. + * + * @param string $encoding Encoding name to test, as per iconv format + * @param bool $bypass Whether or not to bypass the precompiled arrays. + * @return Array of UTF-8 characters to their corresponding ASCII, + * which can be used to "undo" any overzealous iconv action. + */ + public static function testEncodingSupportsASCII($encoding, $bypass = false) + { + // All calls to iconv here are unsafe, proof by case analysis: + // If ICONV_OK, no difference. + // If ICONV_TRUNCATE, all calls involve one character inputs, + // so bug is not triggered. + // If ICONV_UNUSABLE, this call is irrelevant + static $encodings = array(); + if (!$bypass) { + if (isset($encodings[$encoding])) { + return $encodings[$encoding]; + } + $lenc = strtolower($encoding); + switch ($lenc) { + case 'shift_jis': + return array("\xC2\xA5" => '\\', "\xE2\x80\xBE" => '~'); + case 'johab': + return array("\xE2\x82\xA9" => '\\'); + } + if (strpos($lenc, 'iso-8859-') === 0) { + return array(); + } + } + $ret = array(); + if (self::unsafeIconv('UTF-8', $encoding, 'a') === false) { + return false; + } + for ($i = 0x20; $i <= 0x7E; $i++) { // all printable ASCII chars + $c = chr($i); // UTF-8 char + $r = self::unsafeIconv('UTF-8', "$encoding//IGNORE", $c); // initial conversion + if ($r === '' || + // This line is needed for iconv implementations that do not + // omit characters that do not exist in the target character set + ($r === $c && self::unsafeIconv($encoding, 'UTF-8//IGNORE', $r) !== $c) + ) { + // Reverse engineer: what's the UTF-8 equiv of this byte + // sequence? This assumes that there's no variable width + // encoding that doesn't support ASCII. + $ret[self::unsafeIconv($encoding, 'UTF-8//IGNORE', $c)] = $c; + } + } + $encodings[$encoding] = $ret; + return $ret; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/EntityLookup.php b/lib/purifier/HTMLPurifier/EntityLookup.php index cb3474e3be..f12ff13a35 100644 --- a/lib/purifier/HTMLPurifier/EntityLookup.php +++ b/lib/purifier/HTMLPurifier/EntityLookup.php @@ -1,48 +1,48 @@ -table = unserialize(file_get_contents($file)); - } - - /** - * Retrieves sole instance of the object. - * @param bool|HTMLPurifier_EntityLookup $prototype Optional prototype of custom lookup table to overload with. - * @return HTMLPurifier_EntityLookup - */ - public static function instance($prototype = false) - { - // no references, since PHP doesn't copy unless modified - static $instance = null; - if ($prototype) { - $instance = $prototype; - } elseif (!$instance) { - $instance = new HTMLPurifier_EntityLookup(); - $instance->setup(); - } - return $instance; - } -} - -// vim: et sw=4 sts=4 +table = unserialize(file_get_contents($file)); + } + + /** + * Retrieves sole instance of the object. + * @param bool|HTMLPurifier_EntityLookup $prototype Optional prototype of custom lookup table to overload with. + * @return HTMLPurifier_EntityLookup + */ + public static function instance($prototype = false) + { + // no references, since PHP doesn't copy unless modified + static $instance = null; + if ($prototype) { + $instance = $prototype; + } elseif (!$instance) { + $instance = new HTMLPurifier_EntityLookup(); + $instance->setup(); + } + return $instance; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/EntityParser.php b/lib/purifier/HTMLPurifier/EntityParser.php index 41059e50f4..3ef2d09ec6 100644 --- a/lib/purifier/HTMLPurifier/EntityParser.php +++ b/lib/purifier/HTMLPurifier/EntityParser.php @@ -1,285 +1,285 @@ -_semiOptionalPrefixRegex = "/&()()()($semi_optional)/"; - - $this->_textEntitiesRegex = - '/&(?:'. - // hex - '[#]x([a-fA-F0-9]+);?|'. - // dec - '[#]0*(\d+);?|'. - // string (mandatory semicolon) - // NB: order matters: match semicolon preferentially - '([A-Za-z_:][A-Za-z0-9.\-_:]*);|'. - // string (optional semicolon) - "($semi_optional)". - ')/'; - - $this->_attrEntitiesRegex = - '/&(?:'. - // hex - '[#]x([a-fA-F0-9]+);?|'. - // dec - '[#]0*(\d+);?|'. - // string (mandatory semicolon) - // NB: order matters: match semicolon preferentially - '([A-Za-z_:][A-Za-z0-9.\-_:]*);|'. - // string (optional semicolon) - // don't match if trailing is equals or alphanumeric (URL - // like) - "($semi_optional)(?![=;A-Za-z0-9])". - ')/'; - - } - - /** - * Substitute entities with the parsed equivalents. Use this on - * textual data in an HTML document (as opposed to attributes.) - * - * @param string $string String to have entities parsed. - * @return string Parsed string. - */ - public function substituteTextEntities($string) - { - return preg_replace_callback( - $this->_textEntitiesRegex, - array($this, 'entityCallback'), - $string - ); - } - - /** - * Substitute entities with the parsed equivalents. Use this on - * attribute contents in documents. - * - * @param string $string String to have entities parsed. - * @return string Parsed string. - */ - public function substituteAttrEntities($string) - { - return preg_replace_callback( - $this->_attrEntitiesRegex, - array($this, 'entityCallback'), - $string - ); - } - - /** - * Callback function for substituteNonSpecialEntities() that does the work. - * - * @param array $matches PCRE matches array, with 0 the entire match, and - * either index 1, 2 or 3 set with a hex value, dec value, - * or string (respectively). - * @return string Replacement string. - */ - - protected function entityCallback($matches) - { - $entity = $matches[0]; - $hex_part = @$matches[1]; - $dec_part = @$matches[2]; - $named_part = empty($matches[3]) ? @$matches[4] : $matches[3]; - if ($hex_part !== NULL && $hex_part !== "") { - return HTMLPurifier_Encoder::unichr(hexdec($hex_part)); - } elseif ($dec_part !== NULL && $dec_part !== "") { - return HTMLPurifier_Encoder::unichr((int) $dec_part); - } else { - if (!$this->_entity_lookup) { - $this->_entity_lookup = HTMLPurifier_EntityLookup::instance(); - } - if (isset($this->_entity_lookup->table[$named_part])) { - return $this->_entity_lookup->table[$named_part]; - } else { - // exact match didn't match anything, so test if - // any of the semicolon optional match the prefix. - // Test that this is an EXACT match is important to - // prevent infinite loop - if (!empty($matches[3])) { - return preg_replace_callback( - $this->_semiOptionalPrefixRegex, - array($this, 'entityCallback'), - $entity - ); - } - return $entity; - } - } - } - - // LEGACY CODE BELOW - - /** - * Callback regex string for parsing entities. - * @type string - */ - protected $_substituteEntitiesRegex = - '/&(?:[#]x([a-fA-F0-9]+)|[#]0*(\d+)|([A-Za-z_:][A-Za-z0-9.\-_:]*));?/'; - // 1. hex 2. dec 3. string (XML style) - - /** - * Decimal to parsed string conversion table for special entities. - * @type array - */ - protected $_special_dec2str = - array( - 34 => '"', - 38 => '&', - 39 => "'", - 60 => '<', - 62 => '>' - ); - - /** - * Stripped entity names to decimal conversion table for special entities. - * @type array - */ - protected $_special_ent2dec = - array( - 'quot' => 34, - 'amp' => 38, - 'lt' => 60, - 'gt' => 62 - ); - - /** - * Substitutes non-special entities with their parsed equivalents. Since - * running this whenever you have parsed character is t3h 5uck, we run - * it before everything else. - * - * @param string $string String to have non-special entities parsed. - * @return string Parsed string. - */ - public function substituteNonSpecialEntities($string) - { - // it will try to detect missing semicolons, but don't rely on it - return preg_replace_callback( - $this->_substituteEntitiesRegex, - array($this, 'nonSpecialEntityCallback'), - $string - ); - } - - /** - * Callback function for substituteNonSpecialEntities() that does the work. - * - * @param array $matches PCRE matches array, with 0 the entire match, and - * either index 1, 2 or 3 set with a hex value, dec value, - * or string (respectively). - * @return string Replacement string. - */ - - protected function nonSpecialEntityCallback($matches) - { - // replaces all but big five - $entity = $matches[0]; - $is_num = (@$matches[0][1] === '#'); - if ($is_num) { - $is_hex = (@$entity[2] === 'x'); - $code = $is_hex ? hexdec($matches[1]) : (int) $matches[2]; - // abort for special characters - if (isset($this->_special_dec2str[$code])) { - return $entity; - } - return HTMLPurifier_Encoder::unichr($code); - } else { - if (isset($this->_special_ent2dec[$matches[3]])) { - return $entity; - } - if (!$this->_entity_lookup) { - $this->_entity_lookup = HTMLPurifier_EntityLookup::instance(); - } - if (isset($this->_entity_lookup->table[$matches[3]])) { - return $this->_entity_lookup->table[$matches[3]]; - } else { - return $entity; - } - } - } - - /** - * Substitutes only special entities with their parsed equivalents. - * - * @notice We try to avoid calling this function because otherwise, it - * would have to be called a lot (for every parsed section). - * - * @param string $string String to have non-special entities parsed. - * @return string Parsed string. - */ - public function substituteSpecialEntities($string) - { - return preg_replace_callback( - $this->_substituteEntitiesRegex, - array($this, 'specialEntityCallback'), - $string - ); - } - - /** - * Callback function for substituteSpecialEntities() that does the work. - * - * This callback has same syntax as nonSpecialEntityCallback(). - * - * @param array $matches PCRE-style matches array, with 0 the entire match, and - * either index 1, 2 or 3 set with a hex value, dec value, - * or string (respectively). - * @return string Replacement string. - */ - protected function specialEntityCallback($matches) - { - $entity = $matches[0]; - $is_num = (@$matches[0][1] === '#'); - if ($is_num) { - $is_hex = (@$entity[2] === 'x'); - $int = $is_hex ? hexdec($matches[1]) : (int) $matches[2]; - return isset($this->_special_dec2str[$int]) ? - $this->_special_dec2str[$int] : - $entity; - } else { - return isset($this->_special_ent2dec[$matches[3]]) ? - $this->_special_dec2str[$this->_special_ent2dec[$matches[3]]] : - $entity; - } - } -} - -// vim: et sw=4 sts=4 +_semiOptionalPrefixRegex = "/&()()()($semi_optional)/"; + + $this->_textEntitiesRegex = + '/&(?:'. + // hex + '[#]x([a-fA-F0-9]+);?|'. + // dec + '[#]0*(\d+);?|'. + // string (mandatory semicolon) + // NB: order matters: match semicolon preferentially + '([A-Za-z_:][A-Za-z0-9.\-_:]*);|'. + // string (optional semicolon) + "($semi_optional)". + ')/'; + + $this->_attrEntitiesRegex = + '/&(?:'. + // hex + '[#]x([a-fA-F0-9]+);?|'. + // dec + '[#]0*(\d+);?|'. + // string (mandatory semicolon) + // NB: order matters: match semicolon preferentially + '([A-Za-z_:][A-Za-z0-9.\-_:]*);|'. + // string (optional semicolon) + // don't match if trailing is equals or alphanumeric (URL + // like) + "($semi_optional)(?![=;A-Za-z0-9])". + ')/'; + + } + + /** + * Substitute entities with the parsed equivalents. Use this on + * textual data in an HTML document (as opposed to attributes.) + * + * @param string $string String to have entities parsed. + * @return string Parsed string. + */ + public function substituteTextEntities($string) + { + return preg_replace_callback( + $this->_textEntitiesRegex, + array($this, 'entityCallback'), + $string + ); + } + + /** + * Substitute entities with the parsed equivalents. Use this on + * attribute contents in documents. + * + * @param string $string String to have entities parsed. + * @return string Parsed string. + */ + public function substituteAttrEntities($string) + { + return preg_replace_callback( + $this->_attrEntitiesRegex, + array($this, 'entityCallback'), + $string + ); + } + + /** + * Callback function for substituteNonSpecialEntities() that does the work. + * + * @param array $matches PCRE matches array, with 0 the entire match, and + * either index 1, 2 or 3 set with a hex value, dec value, + * or string (respectively). + * @return string Replacement string. + */ + + protected function entityCallback($matches) + { + $entity = $matches[0]; + $hex_part = @$matches[1]; + $dec_part = @$matches[2]; + $named_part = empty($matches[3]) ? (empty($matches[4]) ? "" : $matches[4]) : $matches[3]; + if ($hex_part !== NULL && $hex_part !== "") { + return HTMLPurifier_Encoder::unichr(hexdec($hex_part)); + } elseif ($dec_part !== NULL && $dec_part !== "") { + return HTMLPurifier_Encoder::unichr((int) $dec_part); + } else { + if (!$this->_entity_lookup) { + $this->_entity_lookup = HTMLPurifier_EntityLookup::instance(); + } + if (isset($this->_entity_lookup->table[$named_part])) { + return $this->_entity_lookup->table[$named_part]; + } else { + // exact match didn't match anything, so test if + // any of the semicolon optional match the prefix. + // Test that this is an EXACT match is important to + // prevent infinite loop + if (!empty($matches[3])) { + return preg_replace_callback( + $this->_semiOptionalPrefixRegex, + array($this, 'entityCallback'), + $entity + ); + } + return $entity; + } + } + } + + // LEGACY CODE BELOW + + /** + * Callback regex string for parsing entities. + * @type string + */ + protected $_substituteEntitiesRegex = + '/&(?:[#]x([a-fA-F0-9]+)|[#]0*(\d+)|([A-Za-z_:][A-Za-z0-9.\-_:]*));?/'; + // 1. hex 2. dec 3. string (XML style) + + /** + * Decimal to parsed string conversion table for special entities. + * @type array + */ + protected $_special_dec2str = + array( + 34 => '"', + 38 => '&', + 39 => "'", + 60 => '<', + 62 => '>' + ); + + /** + * Stripped entity names to decimal conversion table for special entities. + * @type array + */ + protected $_special_ent2dec = + array( + 'quot' => 34, + 'amp' => 38, + 'lt' => 60, + 'gt' => 62 + ); + + /** + * Substitutes non-special entities with their parsed equivalents. Since + * running this whenever you have parsed character is t3h 5uck, we run + * it before everything else. + * + * @param string $string String to have non-special entities parsed. + * @return string Parsed string. + */ + public function substituteNonSpecialEntities($string) + { + // it will try to detect missing semicolons, but don't rely on it + return preg_replace_callback( + $this->_substituteEntitiesRegex, + array($this, 'nonSpecialEntityCallback'), + $string + ); + } + + /** + * Callback function for substituteNonSpecialEntities() that does the work. + * + * @param array $matches PCRE matches array, with 0 the entire match, and + * either index 1, 2 or 3 set with a hex value, dec value, + * or string (respectively). + * @return string Replacement string. + */ + + protected function nonSpecialEntityCallback($matches) + { + // replaces all but big five + $entity = $matches[0]; + $is_num = (@$matches[0][1] === '#'); + if ($is_num) { + $is_hex = (@$entity[2] === 'x'); + $code = $is_hex ? hexdec($matches[1]) : (int) $matches[2]; + // abort for special characters + if (isset($this->_special_dec2str[$code])) { + return $entity; + } + return HTMLPurifier_Encoder::unichr($code); + } else { + if (isset($this->_special_ent2dec[$matches[3]])) { + return $entity; + } + if (!$this->_entity_lookup) { + $this->_entity_lookup = HTMLPurifier_EntityLookup::instance(); + } + if (isset($this->_entity_lookup->table[$matches[3]])) { + return $this->_entity_lookup->table[$matches[3]]; + } else { + return $entity; + } + } + } + + /** + * Substitutes only special entities with their parsed equivalents. + * + * @notice We try to avoid calling this function because otherwise, it + * would have to be called a lot (for every parsed section). + * + * @param string $string String to have non-special entities parsed. + * @return string Parsed string. + */ + public function substituteSpecialEntities($string) + { + return preg_replace_callback( + $this->_substituteEntitiesRegex, + array($this, 'specialEntityCallback'), + $string + ); + } + + /** + * Callback function for substituteSpecialEntities() that does the work. + * + * This callback has same syntax as nonSpecialEntityCallback(). + * + * @param array $matches PCRE-style matches array, with 0 the entire match, and + * either index 1, 2 or 3 set with a hex value, dec value, + * or string (respectively). + * @return string Replacement string. + */ + protected function specialEntityCallback($matches) + { + $entity = $matches[0]; + $is_num = (@$matches[0][1] === '#'); + if ($is_num) { + $is_hex = (@$entity[2] === 'x'); + $int = $is_hex ? hexdec($matches[1]) : (int) $matches[2]; + return isset($this->_special_dec2str[$int]) ? + $this->_special_dec2str[$int] : + $entity; + } else { + return isset($this->_special_ent2dec[$matches[3]]) ? + $this->_special_dec2str[$this->_special_ent2dec[$matches[3]]] : + $entity; + } + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ErrorCollector.php b/lib/purifier/HTMLPurifier/ErrorCollector.php index a6cbcaa0ea..d47e3f2e24 100644 --- a/lib/purifier/HTMLPurifier/ErrorCollector.php +++ b/lib/purifier/HTMLPurifier/ErrorCollector.php @@ -1,244 +1,244 @@ -locale =& $context->get('Locale'); - $this->context = $context; - $this->_current =& $this->_stacks[0]; - $this->errors =& $this->_stacks[0]; - } - - /** - * Sends an error message to the collector for later use - * @param int $severity Error severity, PHP error style (don't use E_USER_) - * @param string $msg Error message text - */ - public function send($severity, $msg) - { - $args = array(); - if (func_num_args() > 2) { - $args = func_get_args(); - array_shift($args); - unset($args[0]); - } - - $token = $this->context->get('CurrentToken', true); - $line = $token ? $token->line : $this->context->get('CurrentLine', true); - $col = $token ? $token->col : $this->context->get('CurrentCol', true); - $attr = $this->context->get('CurrentAttr', true); - - // perform special substitutions, also add custom parameters - $subst = array(); - if (!is_null($token)) { - $args['CurrentToken'] = $token; - } - if (!is_null($attr)) { - $subst['$CurrentAttr.Name'] = $attr; - if (isset($token->attr[$attr])) { - $subst['$CurrentAttr.Value'] = $token->attr[$attr]; - } - } - - if (empty($args)) { - $msg = $this->locale->getMessage($msg); - } else { - $msg = $this->locale->formatMessage($msg, $args); - } - - if (!empty($subst)) { - $msg = strtr($msg, $subst); - } - - // (numerically indexed) - $error = array( - self::LINENO => $line, - self::SEVERITY => $severity, - self::MESSAGE => $msg, - self::CHILDREN => array() - ); - $this->_current[] = $error; - - // NEW CODE BELOW ... - // Top-level errors are either: - // TOKEN type, if $value is set appropriately, or - // "syntax" type, if $value is null - $new_struct = new HTMLPurifier_ErrorStruct(); - $new_struct->type = HTMLPurifier_ErrorStruct::TOKEN; - if ($token) { - $new_struct->value = clone $token; - } - if (is_int($line) && is_int($col)) { - if (isset($this->lines[$line][$col])) { - $struct = $this->lines[$line][$col]; - } else { - $struct = $this->lines[$line][$col] = $new_struct; - } - // These ksorts may present a performance problem - ksort($this->lines[$line], SORT_NUMERIC); - } else { - if (isset($this->lines[-1])) { - $struct = $this->lines[-1]; - } else { - $struct = $this->lines[-1] = $new_struct; - } - } - ksort($this->lines, SORT_NUMERIC); - - // Now, check if we need to operate on a lower structure - if (!empty($attr)) { - $struct = $struct->getChild(HTMLPurifier_ErrorStruct::ATTR, $attr); - if (!$struct->value) { - $struct->value = array($attr, 'PUT VALUE HERE'); - } - } - if (!empty($cssprop)) { - $struct = $struct->getChild(HTMLPurifier_ErrorStruct::CSSPROP, $cssprop); - if (!$struct->value) { - // if we tokenize CSS this might be a little more difficult to do - $struct->value = array($cssprop, 'PUT VALUE HERE'); - } - } - - // Ok, structs are all setup, now time to register the error - $struct->addError($severity, $msg); - } - - /** - * Retrieves raw error data for custom formatter to use - */ - public function getRaw() - { - return $this->errors; - } - - /** - * Default HTML formatting implementation for error messages - * @param HTMLPurifier_Config $config Configuration, vital for HTML output nature - * @param array $errors Errors array to display; used for recursion. - * @return string - */ - public function getHTMLFormatted($config, $errors = null) - { - $ret = array(); - - $this->generator = new HTMLPurifier_Generator($config, $this->context); - if ($errors === null) { - $errors = $this->errors; - } - - // 'At line' message needs to be removed - - // generation code for new structure goes here. It needs to be recursive. - foreach ($this->lines as $line => $col_array) { - if ($line == -1) { - continue; - } - foreach ($col_array as $col => $struct) { - $this->_renderStruct($ret, $struct, $line, $col); - } - } - if (isset($this->lines[-1])) { - $this->_renderStruct($ret, $this->lines[-1]); - } - - if (empty($errors)) { - return '

' . $this->locale->getMessage('ErrorCollector: No errors') . '

'; - } else { - return '
  • ' . implode('
  • ', $ret) . '
'; - } - - } - - private function _renderStruct(&$ret, $struct, $line = null, $col = null) - { - $stack = array($struct); - $context_stack = array(array()); - while ($current = array_pop($stack)) { - $context = array_pop($context_stack); - foreach ($current->errors as $error) { - list($severity, $msg) = $error; - $string = ''; - $string .= '
'; - // W3C uses an icon to indicate the severity of the error. - $error = $this->locale->getErrorName($severity); - $string .= "$error "; - if (!is_null($line) && !is_null($col)) { - $string .= "Line $line, Column $col: "; - } else { - $string .= 'End of Document: '; - } - $string .= '' . $this->generator->escape($msg) . ' '; - $string .= '
'; - // Here, have a marker for the character on the column appropriate. - // Be sure to clip extremely long lines. - //$string .= '
';
-                //$string .= '';
-                //$string .= '
'; - $ret[] = $string; - } - foreach ($current->children as $array) { - $context[] = $current; - $stack = array_merge($stack, array_reverse($array, true)); - for ($i = count($array); $i > 0; $i--) { - $context_stack[] = $context; - } - } - } - } -} - -// vim: et sw=4 sts=4 +locale =& $context->get('Locale'); + $this->context = $context; + $this->_current =& $this->_stacks[0]; + $this->errors =& $this->_stacks[0]; + } + + /** + * Sends an error message to the collector for later use + * @param int $severity Error severity, PHP error style (don't use E_USER_) + * @param string $msg Error message text + */ + public function send($severity, $msg) + { + $args = array(); + if (func_num_args() > 2) { + $args = func_get_args(); + array_shift($args); + unset($args[0]); + } + + $token = $this->context->get('CurrentToken', true); + $line = $token ? $token->line : $this->context->get('CurrentLine', true); + $col = $token ? $token->col : $this->context->get('CurrentCol', true); + $attr = $this->context->get('CurrentAttr', true); + + // perform special substitutions, also add custom parameters + $subst = array(); + if (!is_null($token)) { + $args['CurrentToken'] = $token; + } + if (!is_null($attr)) { + $subst['$CurrentAttr.Name'] = $attr; + if (isset($token->attr[$attr])) { + $subst['$CurrentAttr.Value'] = $token->attr[$attr]; + } + } + + if (empty($args)) { + $msg = $this->locale->getMessage($msg); + } else { + $msg = $this->locale->formatMessage($msg, $args); + } + + if (!empty($subst)) { + $msg = strtr($msg, $subst); + } + + // (numerically indexed) + $error = array( + self::LINENO => $line, + self::SEVERITY => $severity, + self::MESSAGE => $msg, + self::CHILDREN => array() + ); + $this->_current[] = $error; + + // NEW CODE BELOW ... + // Top-level errors are either: + // TOKEN type, if $value is set appropriately, or + // "syntax" type, if $value is null + $new_struct = new HTMLPurifier_ErrorStruct(); + $new_struct->type = HTMLPurifier_ErrorStruct::TOKEN; + if ($token) { + $new_struct->value = clone $token; + } + if (is_int($line) && is_int($col)) { + if (isset($this->lines[$line][$col])) { + $struct = $this->lines[$line][$col]; + } else { + $struct = $this->lines[$line][$col] = $new_struct; + } + // These ksorts may present a performance problem + ksort($this->lines[$line], SORT_NUMERIC); + } else { + if (isset($this->lines[-1])) { + $struct = $this->lines[-1]; + } else { + $struct = $this->lines[-1] = $new_struct; + } + } + ksort($this->lines, SORT_NUMERIC); + + // Now, check if we need to operate on a lower structure + if (!empty($attr)) { + $struct = $struct->getChild(HTMLPurifier_ErrorStruct::ATTR, $attr); + if (!$struct->value) { + $struct->value = array($attr, 'PUT VALUE HERE'); + } + } + if (!empty($cssprop)) { + $struct = $struct->getChild(HTMLPurifier_ErrorStruct::CSSPROP, $cssprop); + if (!$struct->value) { + // if we tokenize CSS this might be a little more difficult to do + $struct->value = array($cssprop, 'PUT VALUE HERE'); + } + } + + // Ok, structs are all setup, now time to register the error + $struct->addError($severity, $msg); + } + + /** + * Retrieves raw error data for custom formatter to use + */ + public function getRaw() + { + return $this->errors; + } + + /** + * Default HTML formatting implementation for error messages + * @param HTMLPurifier_Config $config Configuration, vital for HTML output nature + * @param array $errors Errors array to display; used for recursion. + * @return string + */ + public function getHTMLFormatted($config, $errors = null) + { + $ret = array(); + + $this->generator = new HTMLPurifier_Generator($config, $this->context); + if ($errors === null) { + $errors = $this->errors; + } + + // 'At line' message needs to be removed + + // generation code for new structure goes here. It needs to be recursive. + foreach ($this->lines as $line => $col_array) { + if ($line == -1) { + continue; + } + foreach ($col_array as $col => $struct) { + $this->_renderStruct($ret, $struct, $line, $col); + } + } + if (isset($this->lines[-1])) { + $this->_renderStruct($ret, $this->lines[-1]); + } + + if (empty($errors)) { + return '

' . $this->locale->getMessage('ErrorCollector: No errors') . '

'; + } else { + return '
  • ' . implode('
  • ', $ret) . '
'; + } + + } + + private function _renderStruct(&$ret, $struct, $line = null, $col = null) + { + $stack = array($struct); + $context_stack = array(array()); + while ($current = array_pop($stack)) { + $context = array_pop($context_stack); + foreach ($current->errors as $error) { + list($severity, $msg) = $error; + $string = ''; + $string .= '
'; + // W3C uses an icon to indicate the severity of the error. + $error = $this->locale->getErrorName($severity); + $string .= "$error "; + if (!is_null($line) && !is_null($col)) { + $string .= "Line $line, Column $col: "; + } else { + $string .= 'End of Document: '; + } + $string .= '' . $this->generator->escape($msg) . ' '; + $string .= '
'; + // Here, have a marker for the character on the column appropriate. + // Be sure to clip extremely long lines. + //$string .= '
';
+                //$string .= '';
+                //$string .= '
'; + $ret[] = $string; + } + foreach ($current->children as $array) { + $context[] = $current; + $stack = array_merge($stack, array_reverse($array, true)); + for ($i = count($array); $i > 0; $i--) { + $context_stack[] = $context; + } + } + } + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/ErrorStruct.php b/lib/purifier/HTMLPurifier/ErrorStruct.php index a6c0da29e8..cf869d3212 100644 --- a/lib/purifier/HTMLPurifier/ErrorStruct.php +++ b/lib/purifier/HTMLPurifier/ErrorStruct.php @@ -1,74 +1,74 @@ -children[$type][$id])) { - $this->children[$type][$id] = new HTMLPurifier_ErrorStruct(); - $this->children[$type][$id]->type = $type; - } - return $this->children[$type][$id]; - } - - /** - * @param int $severity - * @param string $message - */ - public function addError($severity, $message) - { - $this->errors[] = array($severity, $message); - } -} - -// vim: et sw=4 sts=4 +children[$type][$id])) { + $this->children[$type][$id] = new HTMLPurifier_ErrorStruct(); + $this->children[$type][$id]->type = $type; + } + return $this->children[$type][$id]; + } + + /** + * @param int $severity + * @param string $message + */ + public function addError($severity, $message) + { + $this->errors[] = array($severity, $message); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/Exception.php b/lib/purifier/HTMLPurifier/Exception.php index defe157e75..be85b4c560 100644 --- a/lib/purifier/HTMLPurifier/Exception.php +++ b/lib/purifier/HTMLPurifier/Exception.php @@ -1,12 +1,12 @@ -preFilter, - * 2->preFilter, 3->preFilter, purify, 3->postFilter, 2->postFilter, - * 1->postFilter. - * - * @note Methods are not declared abstract as it is perfectly legitimate - * for an implementation not to want anything to happen on a step - */ - -class HTMLPurifier_Filter -{ - - /** - * Name of the filter for identification purposes. - * @type string - */ - public $name; - - /** - * Pre-processor function, handles HTML before HTML Purifier - * @param string $html - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return string - */ - public function preFilter($html, $config, $context) - { - return $html; - } - - /** - * Post-processor function, handles HTML after HTML Purifier - * @param string $html - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return string - */ - public function postFilter($html, $config, $context) - { - return $html; - } -} - -// vim: et sw=4 sts=4 +preFilter, + * 2->preFilter, 3->preFilter, purify, 3->postFilter, 2->postFilter, + * 1->postFilter. + * + * @note Methods are not declared abstract as it is perfectly legitimate + * for an implementation not to want anything to happen on a step + */ + +class HTMLPurifier_Filter +{ + + /** + * Name of the filter for identification purposes. + * @type string + */ + public $name; + + /** + * Pre-processor function, handles HTML before HTML Purifier + * @param string $html + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + */ + public function preFilter($html, $config, $context) + { + return $html; + } + + /** + * Post-processor function, handles HTML after HTML Purifier + * @param string $html + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + */ + public function postFilter($html, $config, $context) + { + return $html; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/Filter/ExtractStyleBlocks.php b/lib/purifier/HTMLPurifier/Filter/ExtractStyleBlocks.php index 5af24c2020..66f70b0fc0 100644 --- a/lib/purifier/HTMLPurifier/Filter/ExtractStyleBlocks.php +++ b/lib/purifier/HTMLPurifier/Filter/ExtractStyleBlocks.php @@ -1,341 +1,341 @@ - blocks from input HTML, cleans them up - * using CSSTidy, and then places them in $purifier->context->get('StyleBlocks') - * so they can be used elsewhere in the document. - * - * @note - * See tests/HTMLPurifier/Filter/ExtractStyleBlocksTest.php for - * sample usage. - * - * @note - * This filter can also be used on stylesheets not included in the - * document--something purists would probably prefer. Just directly - * call HTMLPurifier_Filter_ExtractStyleBlocks->cleanCSS() - */ -class HTMLPurifier_Filter_ExtractStyleBlocks extends HTMLPurifier_Filter -{ - /** - * @type string - */ - public $name = 'ExtractStyleBlocks'; - - /** - * @type array - */ - private $_styleMatches = array(); - - /** - * @type csstidy - */ - private $_tidy; - - /** - * @type HTMLPurifier_AttrDef_HTML_ID - */ - private $_id_attrdef; - - /** - * @type HTMLPurifier_AttrDef_CSS_Ident - */ - private $_class_attrdef; - - /** - * @type HTMLPurifier_AttrDef_Enum - */ - private $_enum_attrdef; - - public function __construct() - { - $this->_tidy = new csstidy(); - $this->_tidy->set_cfg('lowercase_s', false); - $this->_id_attrdef = new HTMLPurifier_AttrDef_HTML_ID(true); - $this->_class_attrdef = new HTMLPurifier_AttrDef_CSS_Ident(); - $this->_enum_attrdef = new HTMLPurifier_AttrDef_Enum( - array( - 'first-child', - 'link', - 'visited', - 'active', - 'hover', - 'focus' - ) - ); - } - - /** - * Save the contents of CSS blocks to style matches - * @param array $matches preg_replace style $matches array - */ - protected function styleCallback($matches) - { - $this->_styleMatches[] = $matches[1]; - } - - /** - * Removes inline - // we must not grab foo in a font-family prop). - if ($config->get('Filter.ExtractStyleBlocks.Escaping')) { - $css = str_replace( - array('<', '>', '&'), - array('\3C ', '\3E ', '\26 '), - $css - ); - } - return $css; - } -} - -// vim: et sw=4 sts=4 + blocks from input HTML, cleans them up + * using CSSTidy, and then places them in $purifier->context->get('StyleBlocks') + * so they can be used elsewhere in the document. + * + * @note + * See tests/HTMLPurifier/Filter/ExtractStyleBlocksTest.php for + * sample usage. + * + * @note + * This filter can also be used on stylesheets not included in the + * document--something purists would probably prefer. Just directly + * call HTMLPurifier_Filter_ExtractStyleBlocks->cleanCSS() + */ +class HTMLPurifier_Filter_ExtractStyleBlocks extends HTMLPurifier_Filter +{ + /** + * @type string + */ + public $name = 'ExtractStyleBlocks'; + + /** + * @type array + */ + private $_styleMatches = array(); + + /** + * @type csstidy + */ + private $_tidy; + + /** + * @type HTMLPurifier_AttrDef_HTML_ID + */ + private $_id_attrdef; + + /** + * @type HTMLPurifier_AttrDef_CSS_Ident + */ + private $_class_attrdef; + + /** + * @type HTMLPurifier_AttrDef_Enum + */ + private $_enum_attrdef; + + public function __construct() + { + $this->_tidy = new csstidy(); + $this->_tidy->set_cfg('lowercase_s', false); + $this->_id_attrdef = new HTMLPurifier_AttrDef_HTML_ID(true); + $this->_class_attrdef = new HTMLPurifier_AttrDef_CSS_Ident(); + $this->_enum_attrdef = new HTMLPurifier_AttrDef_Enum( + array( + 'first-child', + 'link', + 'visited', + 'active', + 'hover', + 'focus' + ) + ); + } + + /** + * Save the contents of CSS blocks to style matches + * @param array $matches preg_replace style $matches array + */ + protected function styleCallback($matches) + { + $this->_styleMatches[] = $matches[1]; + } + + /** + * Removes inline + // we must not grab foo in a font-family prop). + if ($config->get('Filter.ExtractStyleBlocks.Escaping')) { + $css = str_replace( + array('<', '>', '&'), + array('\3C ', '\3E ', '\26 '), + $css + ); + } + return $css; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/Filter/YouTube.php b/lib/purifier/HTMLPurifier/Filter/YouTube.php index b90ddf7517..276d8362fa 100644 --- a/lib/purifier/HTMLPurifier/Filter/YouTube.php +++ b/lib/purifier/HTMLPurifier/Filter/YouTube.php @@ -1,65 +1,65 @@ -]+>.+?' . - '(?:http:)?//www.youtube.com/((?:v|cp)/[A-Za-z0-9\-_=]+).+?#s'; - $pre_replace = '\1'; - return preg_replace($pre_regex, $pre_replace, $html); - } - - /** - * @param string $html - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return string - */ - public function postFilter($html, $config, $context) - { - $post_regex = '#((?:v|cp)/[A-Za-z0-9\-_=]+)#'; - return preg_replace_callback($post_regex, array($this, 'postFilterCallback'), $html); - } - - /** - * @param $url - * @return string - */ - protected function armorUrl($url) - { - return str_replace('--', '--', $url); - } - - /** - * @param array $matches - * @return string - */ - protected function postFilterCallback($matches) - { - $url = $this->armorUrl($matches[1]); - return '' . - '' . - '' . - ''; - } -} - -// vim: et sw=4 sts=4 +]+>.+?' . + '(?:http:)?//www.youtube.com/((?:v|cp)/[A-Za-z0-9\-_=]+).+?#s'; + $pre_replace = '\1'; + return preg_replace($pre_regex, $pre_replace, $html); + } + + /** + * @param string $html + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return string + */ + public function postFilter($html, $config, $context) + { + $post_regex = '#((?:v|cp)/[A-Za-z0-9\-_=]+)#'; + return preg_replace_callback($post_regex, array($this, 'postFilterCallback'), $html); + } + + /** + * @param $url + * @return string + */ + protected function armorUrl($url) + { + return str_replace('--', '--', $url); + } + + /** + * @param array $matches + * @return string + */ + protected function postFilterCallback($matches) + { + $url = $this->armorUrl($matches[1]); + return '' . + '' . + '' . + ''; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/Generator.php b/lib/purifier/HTMLPurifier/Generator.php index addc23e9e4..eb56e2dfa2 100644 --- a/lib/purifier/HTMLPurifier/Generator.php +++ b/lib/purifier/HTMLPurifier/Generator.php @@ -1,286 +1,286 @@ - tags. - * @type bool - */ - private $_scriptFix = false; - - /** - * Cache of HTMLDefinition during HTML output to determine whether or - * not attributes should be minimized. - * @type HTMLPurifier_HTMLDefinition - */ - private $_def; - - /** - * Cache of %Output.SortAttr. - * @type bool - */ - private $_sortAttr; - - /** - * Cache of %Output.FlashCompat. - * @type bool - */ - private $_flashCompat; - - /** - * Cache of %Output.FixInnerHTML. - * @type bool - */ - private $_innerHTMLFix; - - /** - * Stack for keeping track of object information when outputting IE - * compatibility code. - * @type array - */ - private $_flashStack = array(); - - /** - * Configuration for the generator - * @type HTMLPurifier_Config - */ - protected $config; - - /** - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - */ - public function __construct($config, $context) - { - $this->config = $config; - $this->_scriptFix = $config->get('Output.CommentScriptContents'); - $this->_innerHTMLFix = $config->get('Output.FixInnerHTML'); - $this->_sortAttr = $config->get('Output.SortAttr'); - $this->_flashCompat = $config->get('Output.FlashCompat'); - $this->_def = $config->getHTMLDefinition(); - $this->_xhtml = $this->_def->doctype->xml; - } - - /** - * Generates HTML from an array of tokens. - * @param HTMLPurifier_Token[] $tokens Array of HTMLPurifier_Token - * @return string Generated HTML - */ - public function generateFromTokens($tokens) - { - if (!$tokens) { - return ''; - } - - // Basic algorithm - $html = ''; - for ($i = 0, $size = count($tokens); $i < $size; $i++) { - if ($this->_scriptFix && $tokens[$i]->name === 'script' - && $i + 2 < $size && $tokens[$i+2] instanceof HTMLPurifier_Token_End) { - // script special case - // the contents of the script block must be ONE token - // for this to work. - $html .= $this->generateFromToken($tokens[$i++]); - $html .= $this->generateScriptFromToken($tokens[$i++]); - } - $html .= $this->generateFromToken($tokens[$i]); - } - - // Tidy cleanup - if (extension_loaded('tidy') && $this->config->get('Output.TidyFormat')) { - $tidy = new Tidy; - $tidy->parseString( - $html, - array( - 'indent'=> true, - 'output-xhtml' => $this->_xhtml, - 'show-body-only' => true, - 'indent-spaces' => 2, - 'wrap' => 68, - ), - 'utf8' - ); - $tidy->cleanRepair(); - $html = (string) $tidy; // explicit cast necessary - } - - // Normalize newlines to system defined value - if ($this->config->get('Core.NormalizeNewlines')) { - $nl = $this->config->get('Output.Newline'); - if ($nl === null) { - $nl = PHP_EOL; - } - if ($nl !== "\n") { - $html = str_replace("\n", $nl, $html); - } - } - return $html; - } - - /** - * Generates HTML from a single token. - * @param HTMLPurifier_Token $token HTMLPurifier_Token object. - * @return string Generated HTML - */ - public function generateFromToken($token) - { - if (!$token instanceof HTMLPurifier_Token) { - trigger_error('Cannot generate HTML from non-HTMLPurifier_Token object', E_USER_WARNING); - return ''; - - } elseif ($token instanceof HTMLPurifier_Token_Start) { - $attr = $this->generateAttributes($token->attr, $token->name); - if ($this->_flashCompat) { - if ($token->name == "object") { - $flash = new stdClass(); - $flash->attr = $token->attr; - $flash->param = array(); - $this->_flashStack[] = $flash; - } - } - return '<' . $token->name . ($attr ? ' ' : '') . $attr . '>'; - - } elseif ($token instanceof HTMLPurifier_Token_End) { - $_extra = ''; - if ($this->_flashCompat) { - if ($token->name == "object" && !empty($this->_flashStack)) { - // doesn't do anything for now - } - } - return $_extra . 'name . '>'; - - } elseif ($token instanceof HTMLPurifier_Token_Empty) { - if ($this->_flashCompat && $token->name == "param" && !empty($this->_flashStack)) { - $this->_flashStack[count($this->_flashStack)-1]->param[$token->attr['name']] = $token->attr['value']; - } - $attr = $this->generateAttributes($token->attr, $token->name); - return '<' . $token->name . ($attr ? ' ' : '') . $attr . - ( $this->_xhtml ? ' /': '' ) //
v.
- . '>'; - - } elseif ($token instanceof HTMLPurifier_Token_Text) { - return $this->escape($token->data, ENT_NOQUOTES); - - } elseif ($token instanceof HTMLPurifier_Token_Comment) { - return ''; - } else { - return ''; - - } - } - - /** - * Special case processor for the contents of script tags - * @param HTMLPurifier_Token $token HTMLPurifier_Token object. - * @return string - * @warning This runs into problems if there's already a literal - * --> somewhere inside the script contents. - */ - public function generateScriptFromToken($token) - { - if (!$token instanceof HTMLPurifier_Token_Text) { - return $this->generateFromToken($token); - } - // Thanks - $data = preg_replace('#//\s*$#', '', $token->data); - return ''; - } - - /** - * Generates attribute declarations from attribute array. - * @note This does not include the leading or trailing space. - * @param array $assoc_array_of_attributes Attribute array - * @param string $element Name of element attributes are for, used to check - * attribute minimization. - * @return string Generated HTML fragment for insertion. - */ - public function generateAttributes($assoc_array_of_attributes, $element = '') - { - $html = ''; - if ($this->_sortAttr) { - ksort($assoc_array_of_attributes); - } - foreach ($assoc_array_of_attributes as $key => $value) { - if (!$this->_xhtml) { - // Remove namespaced attributes - if (strpos($key, ':') !== false) { - continue; - } - // Check if we should minimize the attribute: val="val" -> val - if ($element && !empty($this->_def->info[$element]->attr[$key]->minimized)) { - $html .= $key . ' '; - continue; - } - } - // Workaround for Internet Explorer innerHTML bug. - // Essentially, Internet Explorer, when calculating - // innerHTML, omits quotes if there are no instances of - // angled brackets, quotes or spaces. However, when parsing - // HTML (for example, when you assign to innerHTML), it - // treats backticks as quotes. Thus, - // `` - // becomes - // `` - // becomes - // - // Fortunately, all we need to do is trigger an appropriate - // quoting style, which we do by adding an extra space. - // This also is consistent with the W3C spec, which states - // that user agents may ignore leading or trailing - // whitespace (in fact, most don't, at least for attributes - // like alt, but an extra space at the end is barely - // noticeable). Still, we have a configuration knob for - // this, since this transformation is not necesary if you - // don't process user input with innerHTML or you don't plan - // on supporting Internet Explorer. - if ($this->_innerHTMLFix) { - if (strpos($value, '`') !== false) { - // check if correct quoting style would not already be - // triggered - if (strcspn($value, '"\' <>') === strlen($value)) { - // protect! - $value .= ' '; - } - } - } - $html .= $key.'="'.$this->escape($value).'" '; - } - return rtrim($html); - } - - /** - * Escapes raw text data. - * @todo This really ought to be protected, but until we have a facility - * for properly generating HTML here w/o using tokens, it stays - * public. - * @param string $string String data to escape for HTML. - * @param int $quote Quoting style, like htmlspecialchars. ENT_NOQUOTES is - * permissible for non-attribute output. - * @return string escaped data. - */ - public function escape($string, $quote = null) - { - // Workaround for APC bug on Mac Leopard reported by sidepodcast - // http://htmlpurifier.org/phorum/read.php?3,4823,4846 - if ($quote === null) { - $quote = ENT_COMPAT; - } - return htmlspecialchars($string, $quote, 'UTF-8'); - } -} - -// vim: et sw=4 sts=4 + tags. + * @type bool + */ + private $_scriptFix = false; + + /** + * Cache of HTMLDefinition during HTML output to determine whether or + * not attributes should be minimized. + * @type HTMLPurifier_HTMLDefinition + */ + private $_def; + + /** + * Cache of %Output.SortAttr. + * @type bool + */ + private $_sortAttr; + + /** + * Cache of %Output.FlashCompat. + * @type bool + */ + private $_flashCompat; + + /** + * Cache of %Output.FixInnerHTML. + * @type bool + */ + private $_innerHTMLFix; + + /** + * Stack for keeping track of object information when outputting IE + * compatibility code. + * @type array + */ + private $_flashStack = array(); + + /** + * Configuration for the generator + * @type HTMLPurifier_Config + */ + protected $config; + + /** + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + */ + public function __construct($config, $context) + { + $this->config = $config; + $this->_scriptFix = $config->get('Output.CommentScriptContents'); + $this->_innerHTMLFix = $config->get('Output.FixInnerHTML'); + $this->_sortAttr = $config->get('Output.SortAttr'); + $this->_flashCompat = $config->get('Output.FlashCompat'); + $this->_def = $config->getHTMLDefinition(); + $this->_xhtml = $this->_def->doctype->xml; + } + + /** + * Generates HTML from an array of tokens. + * @param HTMLPurifier_Token[] $tokens Array of HTMLPurifier_Token + * @return string Generated HTML + */ + public function generateFromTokens($tokens) + { + if (!$tokens) { + return ''; + } + + // Basic algorithm + $html = ''; + for ($i = 0, $size = count($tokens); $i < $size; $i++) { + if ($this->_scriptFix && $tokens[$i]->name === 'script' + && $i + 2 < $size && $tokens[$i+2] instanceof HTMLPurifier_Token_End) { + // script special case + // the contents of the script block must be ONE token + // for this to work. + $html .= $this->generateFromToken($tokens[$i++]); + $html .= $this->generateScriptFromToken($tokens[$i++]); + } + $html .= $this->generateFromToken($tokens[$i]); + } + + // Tidy cleanup + if (extension_loaded('tidy') && $this->config->get('Output.TidyFormat')) { + $tidy = new Tidy; + $tidy->parseString( + $html, + array( + 'indent'=> true, + 'output-xhtml' => $this->_xhtml, + 'show-body-only' => true, + 'indent-spaces' => 2, + 'wrap' => 68, + ), + 'utf8' + ); + $tidy->cleanRepair(); + $html = (string) $tidy; // explicit cast necessary + } + + // Normalize newlines to system defined value + if ($this->config->get('Core.NormalizeNewlines')) { + $nl = $this->config->get('Output.Newline'); + if ($nl === null) { + $nl = PHP_EOL; + } + if ($nl !== "\n") { + $html = str_replace("\n", $nl, $html); + } + } + return $html; + } + + /** + * Generates HTML from a single token. + * @param HTMLPurifier_Token $token HTMLPurifier_Token object. + * @return string Generated HTML + */ + public function generateFromToken($token) + { + if (!$token instanceof HTMLPurifier_Token) { + trigger_error('Cannot generate HTML from non-HTMLPurifier_Token object', E_USER_WARNING); + return ''; + + } elseif ($token instanceof HTMLPurifier_Token_Start) { + $attr = $this->generateAttributes($token->attr, $token->name); + if ($this->_flashCompat) { + if ($token->name == "object") { + $flash = new stdClass(); + $flash->attr = $token->attr; + $flash->param = array(); + $this->_flashStack[] = $flash; + } + } + return '<' . $token->name . ($attr ? ' ' : '') . $attr . '>'; + + } elseif ($token instanceof HTMLPurifier_Token_End) { + $_extra = ''; + if ($this->_flashCompat) { + if ($token->name == "object" && !empty($this->_flashStack)) { + // doesn't do anything for now + } + } + return $_extra . 'name . '>'; + + } elseif ($token instanceof HTMLPurifier_Token_Empty) { + if ($this->_flashCompat && $token->name == "param" && !empty($this->_flashStack)) { + $this->_flashStack[count($this->_flashStack)-1]->param[$token->attr['name']] = $token->attr['value']; + } + $attr = $this->generateAttributes($token->attr, $token->name); + return '<' . $token->name . ($attr ? ' ' : '') . $attr . + ( $this->_xhtml ? ' /': '' ) //
v.
+ . '>'; + + } elseif ($token instanceof HTMLPurifier_Token_Text) { + return $this->escape($token->data, ENT_NOQUOTES); + + } elseif ($token instanceof HTMLPurifier_Token_Comment) { + return ''; + } else { + return ''; + + } + } + + /** + * Special case processor for the contents of script tags + * @param HTMLPurifier_Token $token HTMLPurifier_Token object. + * @return string + * @warning This runs into problems if there's already a literal + * --> somewhere inside the script contents. + */ + public function generateScriptFromToken($token) + { + if (!$token instanceof HTMLPurifier_Token_Text) { + return $this->generateFromToken($token); + } + // Thanks + $data = preg_replace('#//\s*$#', '', $token->data); + return ''; + } + + /** + * Generates attribute declarations from attribute array. + * @note This does not include the leading or trailing space. + * @param array $assoc_array_of_attributes Attribute array + * @param string $element Name of element attributes are for, used to check + * attribute minimization. + * @return string Generated HTML fragment for insertion. + */ + public function generateAttributes($assoc_array_of_attributes, $element = '') + { + $html = ''; + if ($this->_sortAttr) { + ksort($assoc_array_of_attributes); + } + foreach ($assoc_array_of_attributes as $key => $value) { + if (!$this->_xhtml) { + // Remove namespaced attributes + if (strpos($key, ':') !== false) { + continue; + } + // Check if we should minimize the attribute: val="val" -> val + if ($element && !empty($this->_def->info[$element]->attr[$key]->minimized)) { + $html .= $key . ' '; + continue; + } + } + // Workaround for Internet Explorer innerHTML bug. + // Essentially, Internet Explorer, when calculating + // innerHTML, omits quotes if there are no instances of + // angled brackets, quotes or spaces. However, when parsing + // HTML (for example, when you assign to innerHTML), it + // treats backticks as quotes. Thus, + // `` + // becomes + // `` + // becomes + // + // Fortunately, all we need to do is trigger an appropriate + // quoting style, which we do by adding an extra space. + // This also is consistent with the W3C spec, which states + // that user agents may ignore leading or trailing + // whitespace (in fact, most don't, at least for attributes + // like alt, but an extra space at the end is barely + // noticeable). Still, we have a configuration knob for + // this, since this transformation is not necesary if you + // don't process user input with innerHTML or you don't plan + // on supporting Internet Explorer. + if ($this->_innerHTMLFix) { + if (strpos($value, '`') !== false) { + // check if correct quoting style would not already be + // triggered + if (strcspn($value, '"\' <>') === strlen($value)) { + // protect! + $value .= ' '; + } + } + } + $html .= $key.'="'.$this->escape($value).'" '; + } + return rtrim($html); + } + + /** + * Escapes raw text data. + * @todo This really ought to be protected, but until we have a facility + * for properly generating HTML here w/o using tokens, it stays + * public. + * @param string $string String data to escape for HTML. + * @param int $quote Quoting style, like htmlspecialchars. ENT_NOQUOTES is + * permissible for non-attribute output. + * @return string escaped data. + */ + public function escape($string, $quote = null) + { + // Workaround for APC bug on Mac Leopard reported by sidepodcast + // http://htmlpurifier.org/phorum/read.php?3,4823,4846 + if ($quote === null) { + $quote = ENT_COMPAT; + } + return htmlspecialchars($string, $quote, 'UTF-8'); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/HTMLDefinition.php b/lib/purifier/HTMLPurifier/HTMLDefinition.php index 027c85d56e..9b7b334dd9 100644 --- a/lib/purifier/HTMLPurifier/HTMLDefinition.php +++ b/lib/purifier/HTMLPurifier/HTMLDefinition.php @@ -1,493 +1,493 @@ -getAnonymousModule(); - if (!isset($module->info[$element_name])) { - $element = $module->addBlankElement($element_name); - } else { - $element = $module->info[$element_name]; - } - $element->attr[$attr_name] = $def; - } - - /** - * Adds a custom element to your HTML definition - * @see HTMLPurifier_HTMLModule::addElement() for detailed - * parameter and return value descriptions. - */ - public function addElement($element_name, $type, $contents, $attr_collections, $attributes = array()) - { - $module = $this->getAnonymousModule(); - // assume that if the user is calling this, the element - // is safe. This may not be a good idea - $element = $module->addElement($element_name, $type, $contents, $attr_collections, $attributes); - return $element; - } - - /** - * Adds a blank element to your HTML definition, for overriding - * existing behavior - * @param string $element_name - * @return HTMLPurifier_ElementDef - * @see HTMLPurifier_HTMLModule::addBlankElement() for detailed - * parameter and return value descriptions. - */ - public function addBlankElement($element_name) - { - $module = $this->getAnonymousModule(); - $element = $module->addBlankElement($element_name); - return $element; - } - - /** - * Retrieves a reference to the anonymous module, so you can - * bust out advanced features without having to make your own - * module. - * @return HTMLPurifier_HTMLModule - */ - public function getAnonymousModule() - { - if (!$this->_anonModule) { - $this->_anonModule = new HTMLPurifier_HTMLModule(); - $this->_anonModule->name = 'Anonymous'; - } - return $this->_anonModule; - } - - private $_anonModule = null; - - // PUBLIC BUT INTERNAL VARIABLES -------------------------------------- - - /** - * @type string - */ - public $type = 'HTML'; - - /** - * @type HTMLPurifier_HTMLModuleManager - */ - public $manager; - - /** - * Performs low-cost, preliminary initialization. - */ - public function __construct() - { - $this->manager = new HTMLPurifier_HTMLModuleManager(); - } - - /** - * @param HTMLPurifier_Config $config - */ - protected function doSetup($config) - { - $this->processModules($config); - $this->setupConfigStuff($config); - unset($this->manager); - - // cleanup some of the element definitions - foreach ($this->info as $k => $v) { - unset($this->info[$k]->content_model); - unset($this->info[$k]->content_model_type); - } - } - - /** - * Extract out the information from the manager - * @param HTMLPurifier_Config $config - */ - protected function processModules($config) - { - if ($this->_anonModule) { - // for user specific changes - // this is late-loaded so we don't have to deal with PHP4 - // reference wonky-ness - $this->manager->addModule($this->_anonModule); - unset($this->_anonModule); - } - - $this->manager->setup($config); - $this->doctype = $this->manager->doctype; - - foreach ($this->manager->modules as $module) { - foreach ($module->info_tag_transform as $k => $v) { - if ($v === false) { - unset($this->info_tag_transform[$k]); - } else { - $this->info_tag_transform[$k] = $v; - } - } - foreach ($module->info_attr_transform_pre as $k => $v) { - if ($v === false) { - unset($this->info_attr_transform_pre[$k]); - } else { - $this->info_attr_transform_pre[$k] = $v; - } - } - foreach ($module->info_attr_transform_post as $k => $v) { - if ($v === false) { - unset($this->info_attr_transform_post[$k]); - } else { - $this->info_attr_transform_post[$k] = $v; - } - } - foreach ($module->info_injector as $k => $v) { - if ($v === false) { - unset($this->info_injector[$k]); - } else { - $this->info_injector[$k] = $v; - } - } - } - $this->info = $this->manager->getElements(); - $this->info_content_sets = $this->manager->contentSets->lookup; - } - - /** - * Sets up stuff based on config. We need a better way of doing this. - * @param HTMLPurifier_Config $config - */ - protected function setupConfigStuff($config) - { - $block_wrapper = $config->get('HTML.BlockWrapper'); - if (isset($this->info_content_sets['Block'][$block_wrapper])) { - $this->info_block_wrapper = $block_wrapper; - } else { - trigger_error( - 'Cannot use non-block element as block wrapper', - E_USER_ERROR - ); - } - - $parent = $config->get('HTML.Parent'); - $def = $this->manager->getElement($parent, true); - if ($def) { - $this->info_parent = $parent; - $this->info_parent_def = $def; - } else { - trigger_error( - 'Cannot use unrecognized element as parent', - E_USER_ERROR - ); - $this->info_parent_def = $this->manager->getElement($this->info_parent, true); - } - - // support template text - $support = "(for information on implementing this, see the support forums) "; - - // setup allowed elements ----------------------------------------- - - $allowed_elements = $config->get('HTML.AllowedElements'); - $allowed_attributes = $config->get('HTML.AllowedAttributes'); // retrieve early - - if (!is_array($allowed_elements) && !is_array($allowed_attributes)) { - $allowed = $config->get('HTML.Allowed'); - if (is_string($allowed)) { - list($allowed_elements, $allowed_attributes) = $this->parseTinyMCEAllowedList($allowed); - } - } - - if (is_array($allowed_elements)) { - foreach ($this->info as $name => $d) { - if (!isset($allowed_elements[$name])) { - unset($this->info[$name]); - } - unset($allowed_elements[$name]); - } - // emit errors - foreach ($allowed_elements as $element => $d) { - $element = htmlspecialchars($element); // PHP doesn't escape errors, be careful! - trigger_error("Element '$element' is not supported $support", E_USER_WARNING); - } - } - - // setup allowed attributes --------------------------------------- - - $allowed_attributes_mutable = $allowed_attributes; // by copy! - if (is_array($allowed_attributes)) { - // This actually doesn't do anything, since we went away from - // global attributes. It's possible that userland code uses - // it, but HTMLModuleManager doesn't! - foreach ($this->info_global_attr as $attr => $x) { - $keys = array($attr, "*@$attr", "*.$attr"); - $delete = true; - foreach ($keys as $key) { - if ($delete && isset($allowed_attributes[$key])) { - $delete = false; - } - if (isset($allowed_attributes_mutable[$key])) { - unset($allowed_attributes_mutable[$key]); - } - } - if ($delete) { - unset($this->info_global_attr[$attr]); - } - } - - foreach ($this->info as $tag => $info) { - foreach ($info->attr as $attr => $x) { - $keys = array("$tag@$attr", $attr, "*@$attr", "$tag.$attr", "*.$attr"); - $delete = true; - foreach ($keys as $key) { - if ($delete && isset($allowed_attributes[$key])) { - $delete = false; - } - if (isset($allowed_attributes_mutable[$key])) { - unset($allowed_attributes_mutable[$key]); - } - } - if ($delete) { - if ($this->info[$tag]->attr[$attr]->required) { - trigger_error( - "Required attribute '$attr' in element '$tag' " . - "was not allowed, which means '$tag' will not be allowed either", - E_USER_WARNING - ); - } - unset($this->info[$tag]->attr[$attr]); - } - } - } - // emit errors - foreach ($allowed_attributes_mutable as $elattr => $d) { - $bits = preg_split('/[.@]/', $elattr, 2); - $c = count($bits); - switch ($c) { - case 2: - if ($bits[0] !== '*') { - $element = htmlspecialchars($bits[0]); - $attribute = htmlspecialchars($bits[1]); - if (!isset($this->info[$element])) { - trigger_error( - "Cannot allow attribute '$attribute' if element " . - "'$element' is not allowed/supported $support" - ); - } else { - trigger_error( - "Attribute '$attribute' in element '$element' not supported $support", - E_USER_WARNING - ); - } - break; - } - // otherwise fall through - case 1: - $attribute = htmlspecialchars($bits[0]); - trigger_error( - "Global attribute '$attribute' is not ". - "supported in any elements $support", - E_USER_WARNING - ); - break; - } - } - } - - // setup forbidden elements --------------------------------------- - - $forbidden_elements = $config->get('HTML.ForbiddenElements'); - $forbidden_attributes = $config->get('HTML.ForbiddenAttributes'); - - foreach ($this->info as $tag => $info) { - if (isset($forbidden_elements[$tag])) { - unset($this->info[$tag]); - continue; - } - foreach ($info->attr as $attr => $x) { - if (isset($forbidden_attributes["$tag@$attr"]) || - isset($forbidden_attributes["*@$attr"]) || - isset($forbidden_attributes[$attr]) - ) { - unset($this->info[$tag]->attr[$attr]); - continue; - } elseif (isset($forbidden_attributes["$tag.$attr"])) { // this segment might get removed eventually - // $tag.$attr are not user supplied, so no worries! - trigger_error( - "Error with $tag.$attr: tag.attr syntax not supported for " . - "HTML.ForbiddenAttributes; use tag@attr instead", - E_USER_WARNING - ); - } - } - } - foreach ($forbidden_attributes as $key => $v) { - if (strlen($key) < 2) { - continue; - } - if ($key[0] != '*') { - continue; - } - if ($key[1] == '.') { - trigger_error( - "Error with $key: *.attr syntax not supported for HTML.ForbiddenAttributes; use attr instead", - E_USER_WARNING - ); - } - } - - // setup injectors ----------------------------------------------------- - foreach ($this->info_injector as $i => $injector) { - if ($injector->checkNeeded($config) !== false) { - // remove injector that does not have it's required - // elements/attributes present, and is thus not needed. - unset($this->info_injector[$i]); - } - } - } - - /** - * Parses a TinyMCE-flavored Allowed Elements and Attributes list into - * separate lists for processing. Format is element[attr1|attr2],element2... - * @warning Although it's largely drawn from TinyMCE's implementation, - * it is different, and you'll probably have to modify your lists - * @param array $list String list to parse - * @return array - * @todo Give this its own class, probably static interface - */ - public function parseTinyMCEAllowedList($list) - { - $list = str_replace(array(' ', "\t"), '', $list); - - $elements = array(); - $attributes = array(); - - $chunks = preg_split('/(,|[\n\r]+)/', $list); - foreach ($chunks as $chunk) { - if (empty($chunk)) { - continue; - } - // remove TinyMCE element control characters - if (!strpos($chunk, '[')) { - $element = $chunk; - $attr = false; - } else { - list($element, $attr) = explode('[', $chunk); - } - if ($element !== '*') { - $elements[$element] = true; - } - if (!$attr) { - continue; - } - $attr = substr($attr, 0, strlen($attr) - 1); // remove trailing ] - $attr = explode('|', $attr); - foreach ($attr as $key) { - $attributes["$element.$key"] = true; - } - } - return array($elements, $attributes); - } -} - -// vim: et sw=4 sts=4 +getAnonymousModule(); + if (!isset($module->info[$element_name])) { + $element = $module->addBlankElement($element_name); + } else { + $element = $module->info[$element_name]; + } + $element->attr[$attr_name] = $def; + } + + /** + * Adds a custom element to your HTML definition + * @see HTMLPurifier_HTMLModule::addElement() for detailed + * parameter and return value descriptions. + */ + public function addElement($element_name, $type, $contents, $attr_collections, $attributes = array()) + { + $module = $this->getAnonymousModule(); + // assume that if the user is calling this, the element + // is safe. This may not be a good idea + $element = $module->addElement($element_name, $type, $contents, $attr_collections, $attributes); + return $element; + } + + /** + * Adds a blank element to your HTML definition, for overriding + * existing behavior + * @param string $element_name + * @return HTMLPurifier_ElementDef + * @see HTMLPurifier_HTMLModule::addBlankElement() for detailed + * parameter and return value descriptions. + */ + public function addBlankElement($element_name) + { + $module = $this->getAnonymousModule(); + $element = $module->addBlankElement($element_name); + return $element; + } + + /** + * Retrieves a reference to the anonymous module, so you can + * bust out advanced features without having to make your own + * module. + * @return HTMLPurifier_HTMLModule + */ + public function getAnonymousModule() + { + if (!$this->_anonModule) { + $this->_anonModule = new HTMLPurifier_HTMLModule(); + $this->_anonModule->name = 'Anonymous'; + } + return $this->_anonModule; + } + + private $_anonModule = null; + + // PUBLIC BUT INTERNAL VARIABLES -------------------------------------- + + /** + * @type string + */ + public $type = 'HTML'; + + /** + * @type HTMLPurifier_HTMLModuleManager + */ + public $manager; + + /** + * Performs low-cost, preliminary initialization. + */ + public function __construct() + { + $this->manager = new HTMLPurifier_HTMLModuleManager(); + } + + /** + * @param HTMLPurifier_Config $config + */ + protected function doSetup($config) + { + $this->processModules($config); + $this->setupConfigStuff($config); + unset($this->manager); + + // cleanup some of the element definitions + foreach ($this->info as $k => $v) { + unset($this->info[$k]->content_model); + unset($this->info[$k]->content_model_type); + } + } + + /** + * Extract out the information from the manager + * @param HTMLPurifier_Config $config + */ + protected function processModules($config) + { + if ($this->_anonModule) { + // for user specific changes + // this is late-loaded so we don't have to deal with PHP4 + // reference wonky-ness + $this->manager->addModule($this->_anonModule); + unset($this->_anonModule); + } + + $this->manager->setup($config); + $this->doctype = $this->manager->doctype; + + foreach ($this->manager->modules as $module) { + foreach ($module->info_tag_transform as $k => $v) { + if ($v === false) { + unset($this->info_tag_transform[$k]); + } else { + $this->info_tag_transform[$k] = $v; + } + } + foreach ($module->info_attr_transform_pre as $k => $v) { + if ($v === false) { + unset($this->info_attr_transform_pre[$k]); + } else { + $this->info_attr_transform_pre[$k] = $v; + } + } + foreach ($module->info_attr_transform_post as $k => $v) { + if ($v === false) { + unset($this->info_attr_transform_post[$k]); + } else { + $this->info_attr_transform_post[$k] = $v; + } + } + foreach ($module->info_injector as $k => $v) { + if ($v === false) { + unset($this->info_injector[$k]); + } else { + $this->info_injector[$k] = $v; + } + } + } + $this->info = $this->manager->getElements(); + $this->info_content_sets = $this->manager->contentSets->lookup; + } + + /** + * Sets up stuff based on config. We need a better way of doing this. + * @param HTMLPurifier_Config $config + */ + protected function setupConfigStuff($config) + { + $block_wrapper = $config->get('HTML.BlockWrapper'); + if (isset($this->info_content_sets['Block'][$block_wrapper])) { + $this->info_block_wrapper = $block_wrapper; + } else { + trigger_error( + 'Cannot use non-block element as block wrapper', + E_USER_ERROR + ); + } + + $parent = $config->get('HTML.Parent'); + $def = $this->manager->getElement($parent, true); + if ($def) { + $this->info_parent = $parent; + $this->info_parent_def = $def; + } else { + trigger_error( + 'Cannot use unrecognized element as parent', + E_USER_ERROR + ); + $this->info_parent_def = $this->manager->getElement($this->info_parent, true); + } + + // support template text + $support = "(for information on implementing this, see the support forums) "; + + // setup allowed elements ----------------------------------------- + + $allowed_elements = $config->get('HTML.AllowedElements'); + $allowed_attributes = $config->get('HTML.AllowedAttributes'); // retrieve early + + if (!is_array($allowed_elements) && !is_array($allowed_attributes)) { + $allowed = $config->get('HTML.Allowed'); + if (is_string($allowed)) { + list($allowed_elements, $allowed_attributes) = $this->parseTinyMCEAllowedList($allowed); + } + } + + if (is_array($allowed_elements)) { + foreach ($this->info as $name => $d) { + if (!isset($allowed_elements[$name])) { + unset($this->info[$name]); + } + unset($allowed_elements[$name]); + } + // emit errors + foreach ($allowed_elements as $element => $d) { + $element = htmlspecialchars($element); // PHP doesn't escape errors, be careful! + trigger_error("Element '$element' is not supported $support", E_USER_WARNING); + } + } + + // setup allowed attributes --------------------------------------- + + $allowed_attributes_mutable = $allowed_attributes; // by copy! + if (is_array($allowed_attributes)) { + // This actually doesn't do anything, since we went away from + // global attributes. It's possible that userland code uses + // it, but HTMLModuleManager doesn't! + foreach ($this->info_global_attr as $attr => $x) { + $keys = array($attr, "*@$attr", "*.$attr"); + $delete = true; + foreach ($keys as $key) { + if ($delete && isset($allowed_attributes[$key])) { + $delete = false; + } + if (isset($allowed_attributes_mutable[$key])) { + unset($allowed_attributes_mutable[$key]); + } + } + if ($delete) { + unset($this->info_global_attr[$attr]); + } + } + + foreach ($this->info as $tag => $info) { + foreach ($info->attr as $attr => $x) { + $keys = array("$tag@$attr", $attr, "*@$attr", "$tag.$attr", "*.$attr"); + $delete = true; + foreach ($keys as $key) { + if ($delete && isset($allowed_attributes[$key])) { + $delete = false; + } + if (isset($allowed_attributes_mutable[$key])) { + unset($allowed_attributes_mutable[$key]); + } + } + if ($delete) { + if ($this->info[$tag]->attr[$attr]->required) { + trigger_error( + "Required attribute '$attr' in element '$tag' " . + "was not allowed, which means '$tag' will not be allowed either", + E_USER_WARNING + ); + } + unset($this->info[$tag]->attr[$attr]); + } + } + } + // emit errors + foreach ($allowed_attributes_mutable as $elattr => $d) { + $bits = preg_split('/[.@]/', $elattr, 2); + $c = count($bits); + switch ($c) { + case 2: + if ($bits[0] !== '*') { + $element = htmlspecialchars($bits[0]); + $attribute = htmlspecialchars($bits[1]); + if (!isset($this->info[$element])) { + trigger_error( + "Cannot allow attribute '$attribute' if element " . + "'$element' is not allowed/supported $support" + ); + } else { + trigger_error( + "Attribute '$attribute' in element '$element' not supported $support", + E_USER_WARNING + ); + } + break; + } + // otherwise fall through + case 1: + $attribute = htmlspecialchars($bits[0]); + trigger_error( + "Global attribute '$attribute' is not ". + "supported in any elements $support", + E_USER_WARNING + ); + break; + } + } + } + + // setup forbidden elements --------------------------------------- + + $forbidden_elements = $config->get('HTML.ForbiddenElements'); + $forbidden_attributes = $config->get('HTML.ForbiddenAttributes'); + + foreach ($this->info as $tag => $info) { + if (isset($forbidden_elements[$tag])) { + unset($this->info[$tag]); + continue; + } + foreach ($info->attr as $attr => $x) { + if (isset($forbidden_attributes["$tag@$attr"]) || + isset($forbidden_attributes["*@$attr"]) || + isset($forbidden_attributes[$attr]) + ) { + unset($this->info[$tag]->attr[$attr]); + continue; + } elseif (isset($forbidden_attributes["$tag.$attr"])) { // this segment might get removed eventually + // $tag.$attr are not user supplied, so no worries! + trigger_error( + "Error with $tag.$attr: tag.attr syntax not supported for " . + "HTML.ForbiddenAttributes; use tag@attr instead", + E_USER_WARNING + ); + } + } + } + foreach ($forbidden_attributes as $key => $v) { + if (strlen($key) < 2) { + continue; + } + if ($key[0] != '*') { + continue; + } + if ($key[1] == '.') { + trigger_error( + "Error with $key: *.attr syntax not supported for HTML.ForbiddenAttributes; use attr instead", + E_USER_WARNING + ); + } + } + + // setup injectors ----------------------------------------------------- + foreach ($this->info_injector as $i => $injector) { + if ($injector->checkNeeded($config) !== false) { + // remove injector that does not have it's required + // elements/attributes present, and is thus not needed. + unset($this->info_injector[$i]); + } + } + } + + /** + * Parses a TinyMCE-flavored Allowed Elements and Attributes list into + * separate lists for processing. Format is element[attr1|attr2],element2... + * @warning Although it's largely drawn from TinyMCE's implementation, + * it is different, and you'll probably have to modify your lists + * @param array $list String list to parse + * @return array + * @todo Give this its own class, probably static interface + */ + public function parseTinyMCEAllowedList($list) + { + $list = str_replace(array(' ', "\t"), '', $list); + + $elements = array(); + $attributes = array(); + + $chunks = preg_split('/(,|[\n\r]+)/', $list); + foreach ($chunks as $chunk) { + if (empty($chunk)) { + continue; + } + // remove TinyMCE element control characters + if (!strpos($chunk, '[')) { + $element = $chunk; + $attr = false; + } else { + list($element, $attr) = explode('[', $chunk); + } + if ($element !== '*') { + $elements[$element] = true; + } + if (!$attr) { + continue; + } + $attr = substr($attr, 0, strlen($attr) - 1); // remove trailing ] + $attr = explode('|', $attr); + foreach ($attr as $key) { + $attributes["$element.$key"] = true; + } + } + return array($elements, $attributes); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/HTMLModule.php b/lib/purifier/HTMLPurifier/HTMLModule.php index 9877f9efe9..6d898f80cd 100644 --- a/lib/purifier/HTMLPurifier/HTMLModule.php +++ b/lib/purifier/HTMLPurifier/HTMLModule.php @@ -1,284 +1,284 @@ -info, since the object's data is only info, - * with extra behavior associated with it. - * @type array - */ - public $attr_collections = array(); - - /** - * Associative array of deprecated tag name to HTMLPurifier_TagTransform. - * @type array - */ - public $info_tag_transform = array(); - - /** - * List of HTMLPurifier_AttrTransform to be performed before validation. - * @type array - */ - public $info_attr_transform_pre = array(); - - /** - * List of HTMLPurifier_AttrTransform to be performed after validation. - * @type array - */ - public $info_attr_transform_post = array(); - - /** - * List of HTMLPurifier_Injector to be performed during well-formedness fixing. - * An injector will only be invoked if all of it's pre-requisites are met; - * if an injector fails setup, there will be no error; it will simply be - * silently disabled. - * @type array - */ - public $info_injector = array(); - - /** - * Boolean flag that indicates whether or not getChildDef is implemented. - * For optimization reasons: may save a call to a function. Be sure - * to set it if you do implement getChildDef(), otherwise it will have - * no effect! - * @type bool - */ - public $defines_child_def = false; - - /** - * Boolean flag whether or not this module is safe. If it is not safe, all - * of its members are unsafe. Modules are safe by default (this might be - * slightly dangerous, but it doesn't make much sense to force HTML Purifier, - * which is based off of safe HTML, to explicitly say, "This is safe," even - * though there are modules which are "unsafe") - * - * @type bool - * @note Previously, safety could be applied at an element level granularity. - * We've removed this ability, so in order to add "unsafe" elements - * or attributes, a dedicated module with this property set to false - * must be used. - */ - public $safe = true; - - /** - * Retrieves a proper HTMLPurifier_ChildDef subclass based on - * content_model and content_model_type member variables of - * the HTMLPurifier_ElementDef class. There is a similar function - * in HTMLPurifier_HTMLDefinition. - * @param HTMLPurifier_ElementDef $def - * @return HTMLPurifier_ChildDef subclass - */ - public function getChildDef($def) - { - return false; - } - - // -- Convenience ----------------------------------------------------- - - /** - * Convenience function that sets up a new element - * @param string $element Name of element to add - * @param string|bool $type What content set should element be registered to? - * Set as false to skip this step. - * @param string $contents Allowed children in form of: - * "$content_model_type: $content_model" - * @param array $attr_includes What attribute collections to register to - * element? - * @param array $attr What unique attributes does the element define? - * @see HTMLPurifier_ElementDef:: for in-depth descriptions of these parameters. - * @return HTMLPurifier_ElementDef Created element definition object, so you - * can set advanced parameters - */ - public function addElement($element, $type, $contents, $attr_includes = array(), $attr = array()) - { - $this->elements[] = $element; - // parse content_model - list($content_model_type, $content_model) = $this->parseContents($contents); - // merge in attribute inclusions - $this->mergeInAttrIncludes($attr, $attr_includes); - // add element to content sets - if ($type) { - $this->addElementToContentSet($element, $type); - } - // create element - $this->info[$element] = HTMLPurifier_ElementDef::create( - $content_model, - $content_model_type, - $attr - ); - // literal object $contents means direct child manipulation - if (!is_string($contents)) { - $this->info[$element]->child = $contents; - } - return $this->info[$element]; - } - - /** - * Convenience function that creates a totally blank, non-standalone - * element. - * @param string $element Name of element to create - * @return HTMLPurifier_ElementDef Created element - */ - public function addBlankElement($element) - { - if (!isset($this->info[$element])) { - $this->elements[] = $element; - $this->info[$element] = new HTMLPurifier_ElementDef(); - $this->info[$element]->standalone = false; - } else { - trigger_error("Definition for $element already exists in module, cannot redefine"); - } - return $this->info[$element]; - } - - /** - * Convenience function that registers an element to a content set - * @param string $element Element to register - * @param string $type Name content set (warning: case sensitive, usually upper-case - * first letter) - */ - public function addElementToContentSet($element, $type) - { - if (!isset($this->content_sets[$type])) { - $this->content_sets[$type] = ''; - } else { - $this->content_sets[$type] .= ' | '; - } - $this->content_sets[$type] .= $element; - } - - /** - * Convenience function that transforms single-string contents - * into separate content model and content model type - * @param string $contents Allowed children in form of: - * "$content_model_type: $content_model" - * @return array - * @note If contents is an object, an array of two nulls will be - * returned, and the callee needs to take the original $contents - * and use it directly. - */ - public function parseContents($contents) - { - if (!is_string($contents)) { - return array(null, null); - } // defer - switch ($contents) { - // check for shorthand content model forms - case 'Empty': - return array('empty', ''); - case 'Inline': - return array('optional', 'Inline | #PCDATA'); - case 'Flow': - return array('optional', 'Flow | #PCDATA'); - } - list($content_model_type, $content_model) = explode(':', $contents); - $content_model_type = strtolower(trim($content_model_type)); - $content_model = trim($content_model); - return array($content_model_type, $content_model); - } - - /** - * Convenience function that merges a list of attribute includes into - * an attribute array. - * @param array $attr Reference to attr array to modify - * @param array $attr_includes Array of includes / string include to merge in - */ - public function mergeInAttrIncludes(&$attr, $attr_includes) - { - if (!is_array($attr_includes)) { - if (empty($attr_includes)) { - $attr_includes = array(); - } else { - $attr_includes = array($attr_includes); - } - } - $attr[0] = $attr_includes; - } - - /** - * Convenience function that generates a lookup table with boolean - * true as value. - * @param string $list List of values to turn into a lookup - * @note You can also pass an arbitrary number of arguments in - * place of the regular argument - * @return array array equivalent of list - */ - public function makeLookup($list) - { - if (is_string($list)) { - $list = func_get_args(); - } - $ret = array(); - foreach ($list as $value) { - if (is_null($value)) { - continue; - } - $ret[$value] = true; - } - return $ret; - } - - /** - * Lazy load construction of the module after determining whether - * or not it's needed, and also when a finalized configuration object - * is available. - * @param HTMLPurifier_Config $config - */ - public function setup($config) - { - } -} - -// vim: et sw=4 sts=4 +info, since the object's data is only info, + * with extra behavior associated with it. + * @type array + */ + public $attr_collections = array(); + + /** + * Associative array of deprecated tag name to HTMLPurifier_TagTransform. + * @type array + */ + public $info_tag_transform = array(); + + /** + * List of HTMLPurifier_AttrTransform to be performed before validation. + * @type array + */ + public $info_attr_transform_pre = array(); + + /** + * List of HTMLPurifier_AttrTransform to be performed after validation. + * @type array + */ + public $info_attr_transform_post = array(); + + /** + * List of HTMLPurifier_Injector to be performed during well-formedness fixing. + * An injector will only be invoked if all of it's pre-requisites are met; + * if an injector fails setup, there will be no error; it will simply be + * silently disabled. + * @type array + */ + public $info_injector = array(); + + /** + * Boolean flag that indicates whether or not getChildDef is implemented. + * For optimization reasons: may save a call to a function. Be sure + * to set it if you do implement getChildDef(), otherwise it will have + * no effect! + * @type bool + */ + public $defines_child_def = false; + + /** + * Boolean flag whether or not this module is safe. If it is not safe, all + * of its members are unsafe. Modules are safe by default (this might be + * slightly dangerous, but it doesn't make much sense to force HTML Purifier, + * which is based off of safe HTML, to explicitly say, "This is safe," even + * though there are modules which are "unsafe") + * + * @type bool + * @note Previously, safety could be applied at an element level granularity. + * We've removed this ability, so in order to add "unsafe" elements + * or attributes, a dedicated module with this property set to false + * must be used. + */ + public $safe = true; + + /** + * Retrieves a proper HTMLPurifier_ChildDef subclass based on + * content_model and content_model_type member variables of + * the HTMLPurifier_ElementDef class. There is a similar function + * in HTMLPurifier_HTMLDefinition. + * @param HTMLPurifier_ElementDef $def + * @return HTMLPurifier_ChildDef subclass + */ + public function getChildDef($def) + { + return false; + } + + // -- Convenience ----------------------------------------------------- + + /** + * Convenience function that sets up a new element + * @param string $element Name of element to add + * @param string|bool $type What content set should element be registered to? + * Set as false to skip this step. + * @param string|HTMLPurifier_ChildDef $contents Allowed children in form of: + * "$content_model_type: $content_model" + * @param array|string $attr_includes What attribute collections to register to + * element? + * @param array $attr What unique attributes does the element define? + * @see HTMLPurifier_ElementDef:: for in-depth descriptions of these parameters. + * @return HTMLPurifier_ElementDef Created element definition object, so you + * can set advanced parameters + */ + public function addElement($element, $type, $contents, $attr_includes = array(), $attr = array()) + { + $this->elements[] = $element; + // parse content_model + list($content_model_type, $content_model) = $this->parseContents($contents); + // merge in attribute inclusions + $this->mergeInAttrIncludes($attr, $attr_includes); + // add element to content sets + if ($type) { + $this->addElementToContentSet($element, $type); + } + // create element + $this->info[$element] = HTMLPurifier_ElementDef::create( + $content_model, + $content_model_type, + $attr + ); + // literal object $contents means direct child manipulation + if (!is_string($contents)) { + $this->info[$element]->child = $contents; + } + return $this->info[$element]; + } + + /** + * Convenience function that creates a totally blank, non-standalone + * element. + * @param string $element Name of element to create + * @return HTMLPurifier_ElementDef Created element + */ + public function addBlankElement($element) + { + if (!isset($this->info[$element])) { + $this->elements[] = $element; + $this->info[$element] = new HTMLPurifier_ElementDef(); + $this->info[$element]->standalone = false; + } else { + trigger_error("Definition for $element already exists in module, cannot redefine"); + } + return $this->info[$element]; + } + + /** + * Convenience function that registers an element to a content set + * @param string $element Element to register + * @param string $type Name content set (warning: case sensitive, usually upper-case + * first letter) + */ + public function addElementToContentSet($element, $type) + { + if (!isset($this->content_sets[$type])) { + $this->content_sets[$type] = ''; + } else { + $this->content_sets[$type] .= ' | '; + } + $this->content_sets[$type] .= $element; + } + + /** + * Convenience function that transforms single-string contents + * into separate content model and content model type + * @param string $contents Allowed children in form of: + * "$content_model_type: $content_model" + * @return array + * @note If contents is an object, an array of two nulls will be + * returned, and the callee needs to take the original $contents + * and use it directly. + */ + public function parseContents($contents) + { + if (!is_string($contents)) { + return array(null, null); + } // defer + switch ($contents) { + // check for shorthand content model forms + case 'Empty': + return array('empty', ''); + case 'Inline': + return array('optional', 'Inline | #PCDATA'); + case 'Flow': + return array('optional', 'Flow | #PCDATA'); + } + list($content_model_type, $content_model) = explode(':', $contents); + $content_model_type = strtolower(trim($content_model_type)); + $content_model = trim($content_model); + return array($content_model_type, $content_model); + } + + /** + * Convenience function that merges a list of attribute includes into + * an attribute array. + * @param array $attr Reference to attr array to modify + * @param array $attr_includes Array of includes / string include to merge in + */ + public function mergeInAttrIncludes(&$attr, $attr_includes) + { + if (!is_array($attr_includes)) { + if (empty($attr_includes)) { + $attr_includes = array(); + } else { + $attr_includes = array($attr_includes); + } + } + $attr[0] = $attr_includes; + } + + /** + * Convenience function that generates a lookup table with boolean + * true as value. + * @param string $list List of values to turn into a lookup + * @note You can also pass an arbitrary number of arguments in + * place of the regular argument + * @return array array equivalent of list + */ + public function makeLookup($list) + { + if (is_string($list)) { + $list = func_get_args(); + } + $ret = array(); + foreach ($list as $value) { + if (is_null($value)) { + continue; + } + $ret[$value] = true; + } + return $ret; + } + + /** + * Lazy load construction of the module after determining whether + * or not it's needed, and also when a finalized configuration object + * is available. + * @param HTMLPurifier_Config $config + */ + public function setup($config) + { + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/HTMLModule/Bdo.php b/lib/purifier/HTMLPurifier/HTMLModule/Bdo.php index 191a78d1cc..1e67c790d0 100644 --- a/lib/purifier/HTMLPurifier/HTMLModule/Bdo.php +++ b/lib/purifier/HTMLPurifier/HTMLModule/Bdo.php @@ -1,44 +1,44 @@ - array('dir' => false) - ); - - /** - * @param HTMLPurifier_Config $config - */ - public function setup($config) - { - $bdo = $this->addElement( - 'bdo', - 'Inline', - 'Inline', - array('Core', 'Lang'), - array( - 'dir' => 'Enum#ltr,rtl', // required - // The Abstract Module specification has the attribute - // inclusions wrong for bdo: bdo allows Lang - ) - ); - $bdo->attr_transform_post[] = new HTMLPurifier_AttrTransform_BdoDir(); - - $this->attr_collections['I18N']['dir'] = 'Enum#ltr,rtl'; - } -} - -// vim: et sw=4 sts=4 + array('dir' => false) + ); + + /** + * @param HTMLPurifier_Config $config + */ + public function setup($config) + { + $bdo = $this->addElement( + 'bdo', + 'Inline', + 'Inline', + array('Core', 'Lang'), + array( + 'dir' => 'Enum#ltr,rtl', // required + // The Abstract Module specification has the attribute + // inclusions wrong for bdo: bdo allows Lang + ) + ); + $bdo->attr_transform_post[] = new HTMLPurifier_AttrTransform_BdoDir(); + + $this->attr_collections['I18N']['dir'] = 'Enum#ltr,rtl'; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/HTMLModule/CommonAttributes.php b/lib/purifier/HTMLPurifier/HTMLModule/CommonAttributes.php index e2fe53fc81..a96ab1bef1 100644 --- a/lib/purifier/HTMLPurifier/HTMLModule/CommonAttributes.php +++ b/lib/purifier/HTMLPurifier/HTMLModule/CommonAttributes.php @@ -1,31 +1,31 @@ - array( - 0 => array('Style'), - // 'xml:space' => false, - 'class' => 'Class', - 'id' => 'ID', - 'title' => 'CDATA', - ), - 'Lang' => array(), - 'I18N' => array( - 0 => array('Lang'), // proprietary, for xml:lang/lang - ), - 'Common' => array( - 0 => array('Core', 'I18N') - ) - ); -} - -// vim: et sw=4 sts=4 + array( + 0 => array('Style'), + // 'xml:space' => false, + 'class' => 'Class', + 'id' => 'ID', + 'title' => 'CDATA', + ), + 'Lang' => array(), + 'I18N' => array( + 0 => array('Lang'), // proprietary, for xml:lang/lang + ), + 'Common' => array( + 0 => array('Core', 'I18N') + ) + ); +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/HTMLModule/Edit.php b/lib/purifier/HTMLPurifier/HTMLModule/Edit.php index b8288368cf..a9042a3577 100644 --- a/lib/purifier/HTMLPurifier/HTMLModule/Edit.php +++ b/lib/purifier/HTMLPurifier/HTMLModule/Edit.php @@ -1,55 +1,55 @@ - 'URI', - // 'datetime' => 'Datetime', // not implemented - ); - $this->addElement('del', 'Inline', $contents, 'Common', $attr); - $this->addElement('ins', 'Inline', $contents, 'Common', $attr); - } - - // HTML 4.01 specifies that ins/del must not contain block - // elements when used in an inline context, chameleon is - // a complicated workaround to acheive this effect - - // Inline context ! Block context (exclamation mark is - // separator, see getChildDef for parsing) - - /** - * @type bool - */ - public $defines_child_def = true; - - /** - * @param HTMLPurifier_ElementDef $def - * @return HTMLPurifier_ChildDef_Chameleon - */ - public function getChildDef($def) - { - if ($def->content_model_type != 'chameleon') { - return false; - } - $value = explode('!', $def->content_model); - return new HTMLPurifier_ChildDef_Chameleon($value[0], $value[1]); - } -} - -// vim: et sw=4 sts=4 + 'URI', + // 'datetime' => 'Datetime', // not implemented + ); + $this->addElement('del', 'Inline', $contents, 'Common', $attr); + $this->addElement('ins', 'Inline', $contents, 'Common', $attr); + } + + // HTML 4.01 specifies that ins/del must not contain block + // elements when used in an inline context, chameleon is + // a complicated workaround to acheive this effect + + // Inline context ! Block context (exclamation mark is + // separator, see getChildDef for parsing) + + /** + * @type bool + */ + public $defines_child_def = true; + + /** + * @param HTMLPurifier_ElementDef $def + * @return HTMLPurifier_ChildDef_Chameleon + */ + public function getChildDef($def) + { + if ($def->content_model_type != 'chameleon') { + return false; + } + $value = explode('!', $def->content_model); + return new HTMLPurifier_ChildDef_Chameleon($value[0], $value[1]); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/HTMLModule/Forms.php b/lib/purifier/HTMLPurifier/HTMLModule/Forms.php index 13ce6ad5d9..eb0edcffd5 100644 --- a/lib/purifier/HTMLPurifier/HTMLModule/Forms.php +++ b/lib/purifier/HTMLPurifier/HTMLModule/Forms.php @@ -1,190 +1,194 @@ - 'Form', - 'Inline' => 'Formctrl', - ); - - /** - * @param HTMLPurifier_Config $config - */ - public function setup($config) - { - $form = $this->addElement( - 'form', - 'Form', - 'Required: Heading | List | Block | fieldset', - 'Common', - array( - 'accept' => 'ContentTypes', - 'accept-charset' => 'Charsets', - 'action*' => 'URI', - 'method' => 'Enum#get,post', - // really ContentType, but these two are the only ones used today - 'enctype' => 'Enum#application/x-www-form-urlencoded,multipart/form-data', - ) - ); - $form->excludes = array('form' => true); - - $input = $this->addElement( - 'input', - 'Formctrl', - 'Empty', - 'Common', - array( - 'accept' => 'ContentTypes', - 'accesskey' => 'Character', - 'alt' => 'Text', - 'checked' => 'Bool#checked', - 'disabled' => 'Bool#disabled', - 'maxlength' => 'Number', - 'name' => 'CDATA', - 'readonly' => 'Bool#readonly', - 'size' => 'Number', - 'src' => 'URI#embedded', - 'tabindex' => 'Number', - 'type' => 'Enum#text,password,checkbox,button,radio,submit,reset,file,hidden,image', - 'value' => 'CDATA', - ) - ); - $input->attr_transform_post[] = new HTMLPurifier_AttrTransform_Input(); - - $this->addElement( - 'select', - 'Formctrl', - 'Required: optgroup | option', - 'Common', - array( - 'disabled' => 'Bool#disabled', - 'multiple' => 'Bool#multiple', - 'name' => 'CDATA', - 'size' => 'Number', - 'tabindex' => 'Number', - ) - ); - - $this->addElement( - 'option', - false, - 'Optional: #PCDATA', - 'Common', - array( - 'disabled' => 'Bool#disabled', - 'label' => 'Text', - 'selected' => 'Bool#selected', - 'value' => 'CDATA', - ) - ); - // It's illegal for there to be more than one selected, but not - // be multiple. Also, no selected means undefined behavior. This might - // be difficult to implement; perhaps an injector, or a context variable. - - $textarea = $this->addElement( - 'textarea', - 'Formctrl', - 'Optional: #PCDATA', - 'Common', - array( - 'accesskey' => 'Character', - 'cols*' => 'Number', - 'disabled' => 'Bool#disabled', - 'name' => 'CDATA', - 'readonly' => 'Bool#readonly', - 'rows*' => 'Number', - 'tabindex' => 'Number', - ) - ); - $textarea->attr_transform_pre[] = new HTMLPurifier_AttrTransform_Textarea(); - - $button = $this->addElement( - 'button', - 'Formctrl', - 'Optional: #PCDATA | Heading | List | Block | Inline', - 'Common', - array( - 'accesskey' => 'Character', - 'disabled' => 'Bool#disabled', - 'name' => 'CDATA', - 'tabindex' => 'Number', - 'type' => 'Enum#button,submit,reset', - 'value' => 'CDATA', - ) - ); - - // For exclusions, ideally we'd specify content sets, not literal elements - $button->excludes = $this->makeLookup( - 'form', - 'fieldset', // Form - 'input', - 'select', - 'textarea', - 'label', - 'button', // Formctrl - 'a', // as per HTML 4.01 spec, this is omitted by modularization - 'isindex', - 'iframe' // legacy items - ); - - // Extra exclusion: img usemap="" is not permitted within this element. - // We'll omit this for now, since we don't have any good way of - // indicating it yet. - - // This is HIGHLY user-unfriendly; we need a custom child-def for this - $this->addElement('fieldset', 'Form', 'Custom: (#WS?,legend,(Flow|#PCDATA)*)', 'Common'); - - $label = $this->addElement( - 'label', - 'Formctrl', - 'Optional: #PCDATA | Inline', - 'Common', - array( - 'accesskey' => 'Character', - // 'for' => 'IDREF', // IDREF not implemented, cannot allow - ) - ); - $label->excludes = array('label' => true); - - $this->addElement( - 'legend', - false, - 'Optional: #PCDATA | Inline', - 'Common', - array( - 'accesskey' => 'Character', - ) - ); - - $this->addElement( - 'optgroup', - false, - 'Required: option', - 'Common', - array( - 'disabled' => 'Bool#disabled', - 'label*' => 'Text', - ) - ); - // Don't forget an injector for . This one's a little complex - // because it maps to multiple elements. - } -} - -// vim: et sw=4 sts=4 + 'Form', + 'Inline' => 'Formctrl', + ); + + /** + * @param HTMLPurifier_Config $config + */ + public function setup($config) + { + if ($config->get('HTML.Forms')) { + $this->safe = true; + } + + $form = $this->addElement( + 'form', + 'Form', + 'Required: Heading | List | Block | fieldset', + 'Common', + array( + 'accept' => 'ContentTypes', + 'accept-charset' => 'Charsets', + 'action*' => 'URI', + 'method' => 'Enum#get,post', + // really ContentType, but these two are the only ones used today + 'enctype' => 'Enum#application/x-www-form-urlencoded,multipart/form-data', + ) + ); + $form->excludes = array('form' => true); + + $input = $this->addElement( + 'input', + 'Formctrl', + 'Empty', + 'Common', + array( + 'accept' => 'ContentTypes', + 'accesskey' => 'Character', + 'alt' => 'Text', + 'checked' => 'Bool#checked', + 'disabled' => 'Bool#disabled', + 'maxlength' => 'Number', + 'name' => 'CDATA', + 'readonly' => 'Bool#readonly', + 'size' => 'Number', + 'src' => 'URI#embedded', + 'tabindex' => 'Number', + 'type' => 'Enum#text,password,checkbox,button,radio,submit,reset,file,hidden,image', + 'value' => 'CDATA', + ) + ); + $input->attr_transform_post[] = new HTMLPurifier_AttrTransform_Input(); + + $this->addElement( + 'select', + 'Formctrl', + 'Required: optgroup | option', + 'Common', + array( + 'disabled' => 'Bool#disabled', + 'multiple' => 'Bool#multiple', + 'name' => 'CDATA', + 'size' => 'Number', + 'tabindex' => 'Number', + ) + ); + + $this->addElement( + 'option', + false, + 'Optional: #PCDATA', + 'Common', + array( + 'disabled' => 'Bool#disabled', + 'label' => 'Text', + 'selected' => 'Bool#selected', + 'value' => 'CDATA', + ) + ); + // It's illegal for there to be more than one selected, but not + // be multiple. Also, no selected means undefined behavior. This might + // be difficult to implement; perhaps an injector, or a context variable. + + $textarea = $this->addElement( + 'textarea', + 'Formctrl', + 'Optional: #PCDATA', + 'Common', + array( + 'accesskey' => 'Character', + 'cols*' => 'Number', + 'disabled' => 'Bool#disabled', + 'name' => 'CDATA', + 'readonly' => 'Bool#readonly', + 'rows*' => 'Number', + 'tabindex' => 'Number', + ) + ); + $textarea->attr_transform_pre[] = new HTMLPurifier_AttrTransform_Textarea(); + + $button = $this->addElement( + 'button', + 'Formctrl', + 'Optional: #PCDATA | Heading | List | Block | Inline', + 'Common', + array( + 'accesskey' => 'Character', + 'disabled' => 'Bool#disabled', + 'name' => 'CDATA', + 'tabindex' => 'Number', + 'type' => 'Enum#button,submit,reset', + 'value' => 'CDATA', + ) + ); + + // For exclusions, ideally we'd specify content sets, not literal elements + $button->excludes = $this->makeLookup( + 'form', + 'fieldset', // Form + 'input', + 'select', + 'textarea', + 'label', + 'button', // Formctrl + 'a', // as per HTML 4.01 spec, this is omitted by modularization + 'isindex', + 'iframe' // legacy items + ); + + // Extra exclusion: img usemap="" is not permitted within this element. + // We'll omit this for now, since we don't have any good way of + // indicating it yet. + + // This is HIGHLY user-unfriendly; we need a custom child-def for this + $this->addElement('fieldset', 'Form', 'Custom: (#WS?,legend,(Flow|#PCDATA)*)', 'Common'); + + $label = $this->addElement( + 'label', + 'Formctrl', + 'Optional: #PCDATA | Inline', + 'Common', + array( + 'accesskey' => 'Character', + // 'for' => 'IDREF', // IDREF not implemented, cannot allow + ) + ); + $label->excludes = array('label' => true); + + $this->addElement( + 'legend', + false, + 'Optional: #PCDATA | Inline', + 'Common', + array( + 'accesskey' => 'Character', + ) + ); + + $this->addElement( + 'optgroup', + false, + 'Required: option', + 'Common', + array( + 'disabled' => 'Bool#disabled', + 'label*' => 'Text', + ) + ); + // Don't forget an injector for . This one's a little complex + // because it maps to multiple elements. + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/HTMLModule/Hypertext.php b/lib/purifier/HTMLPurifier/HTMLModule/Hypertext.php index 968c07e980..72d7a31e68 100644 --- a/lib/purifier/HTMLPurifier/HTMLModule/Hypertext.php +++ b/lib/purifier/HTMLPurifier/HTMLModule/Hypertext.php @@ -1,40 +1,40 @@ -addElement( - 'a', - 'Inline', - 'Inline', - 'Common', - array( - // 'accesskey' => 'Character', - // 'charset' => 'Charset', - 'href' => 'URI', - // 'hreflang' => 'LanguageCode', - 'rel' => new HTMLPurifier_AttrDef_HTML_LinkTypes('rel'), - 'rev' => new HTMLPurifier_AttrDef_HTML_LinkTypes('rev'), - // 'tabindex' => 'Number', - // 'type' => 'ContentType', - ) - ); - $a->formatting = true; - $a->excludes = array('a' => true); - } -} - -// vim: et sw=4 sts=4 +addElement( + 'a', + 'Inline', + 'Inline', + 'Common', + array( + // 'accesskey' => 'Character', + // 'charset' => 'Charset', + 'href' => 'URI', + // 'hreflang' => 'LanguageCode', + 'rel' => new HTMLPurifier_AttrDef_HTML_LinkTypes('rel'), + 'rev' => new HTMLPurifier_AttrDef_HTML_LinkTypes('rev'), + // 'tabindex' => 'Number', + // 'type' => 'ContentType', + ) + ); + $a->formatting = true; + $a->excludes = array('a' => true); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/HTMLModule/Iframe.php b/lib/purifier/HTMLPurifier/HTMLModule/Iframe.php index 2c9bdc5803..f7e7c91c02 100644 --- a/lib/purifier/HTMLPurifier/HTMLModule/Iframe.php +++ b/lib/purifier/HTMLPurifier/HTMLModule/Iframe.php @@ -1,51 +1,51 @@ -get('HTML.SafeIframe')) { - $this->safe = true; - } - $this->addElement( - 'iframe', - 'Inline', - 'Flow', - 'Common', - array( - 'src' => 'URI#embedded', - 'width' => 'Length', - 'height' => 'Length', - 'name' => 'ID', - 'scrolling' => 'Enum#yes,no,auto', - 'frameborder' => 'Enum#0,1', - 'longdesc' => 'URI', - 'marginheight' => 'Pixels', - 'marginwidth' => 'Pixels', - ) - ); - } -} - -// vim: et sw=4 sts=4 +get('HTML.SafeIframe')) { + $this->safe = true; + } + $this->addElement( + 'iframe', + 'Inline', + 'Flow', + 'Common', + array( + 'src' => 'URI#embedded', + 'width' => 'Length', + 'height' => 'Length', + 'name' => 'ID', + 'scrolling' => 'Enum#yes,no,auto', + 'frameborder' => 'Enum#0,1', + 'longdesc' => 'URI', + 'marginheight' => 'Pixels', + 'marginwidth' => 'Pixels', + ) + ); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/HTMLModule/Image.php b/lib/purifier/HTMLPurifier/HTMLModule/Image.php index 0ed7411e7f..0f5fdb3baf 100644 --- a/lib/purifier/HTMLPurifier/HTMLModule/Image.php +++ b/lib/purifier/HTMLPurifier/HTMLModule/Image.php @@ -1,49 +1,49 @@ -get('HTML.MaxImgLength'); - $img = $this->addElement( - 'img', - 'Inline', - 'Empty', - 'Common', - array( - 'alt*' => 'Text', - // According to the spec, it's Length, but percents can - // be abused, so we allow only Pixels. - 'height' => 'Pixels#' . $max, - 'width' => 'Pixels#' . $max, - 'longdesc' => 'URI', - 'src*' => new HTMLPurifier_AttrDef_URI(true), // embedded - ) - ); - if ($max === null || $config->get('HTML.Trusted')) { - $img->attr['height'] = - $img->attr['width'] = 'Length'; - } - - // kind of strange, but splitting things up would be inefficient - $img->attr_transform_pre[] = - $img->attr_transform_post[] = - new HTMLPurifier_AttrTransform_ImgRequired(); - } -} - -// vim: et sw=4 sts=4 +get('HTML.MaxImgLength'); + $img = $this->addElement( + 'img', + 'Inline', + 'Empty', + 'Common', + array( + 'alt*' => 'Text', + // According to the spec, it's Length, but percents can + // be abused, so we allow only Pixels. + 'height' => 'Pixels#' . $max, + 'width' => 'Pixels#' . $max, + 'longdesc' => 'URI', + 'src*' => new HTMLPurifier_AttrDef_URI(true), // embedded + ) + ); + if ($max === null || $config->get('HTML.Trusted')) { + $img->attr['height'] = + $img->attr['width'] = 'Length'; + } + + // kind of strange, but splitting things up would be inefficient + $img->attr_transform_pre[] = + $img->attr_transform_post[] = + new HTMLPurifier_AttrTransform_ImgRequired(); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/HTMLModule/Legacy.php b/lib/purifier/HTMLPurifier/HTMLModule/Legacy.php index 9ca1cb3756..86b5299579 100644 --- a/lib/purifier/HTMLPurifier/HTMLModule/Legacy.php +++ b/lib/purifier/HTMLPurifier/HTMLModule/Legacy.php @@ -1,186 +1,186 @@ -addElement( - 'basefont', - 'Inline', - 'Empty', - null, - array( - 'color' => 'Color', - 'face' => 'Text', // extremely broad, we should - 'size' => 'Text', // tighten it - 'id' => 'ID' - ) - ); - $this->addElement('center', 'Block', 'Flow', 'Common'); - $this->addElement( - 'dir', - 'Block', - 'Required: li', - 'Common', - array( - 'compact' => 'Bool#compact' - ) - ); - $this->addElement( - 'font', - 'Inline', - 'Inline', - array('Core', 'I18N'), - array( - 'color' => 'Color', - 'face' => 'Text', // extremely broad, we should - 'size' => 'Text', // tighten it - ) - ); - $this->addElement( - 'menu', - 'Block', - 'Required: li', - 'Common', - array( - 'compact' => 'Bool#compact' - ) - ); - - $s = $this->addElement('s', 'Inline', 'Inline', 'Common'); - $s->formatting = true; - - $strike = $this->addElement('strike', 'Inline', 'Inline', 'Common'); - $strike->formatting = true; - - $u = $this->addElement('u', 'Inline', 'Inline', 'Common'); - $u->formatting = true; - - // setup modifications to old elements - - $align = 'Enum#left,right,center,justify'; - - $address = $this->addBlankElement('address'); - $address->content_model = 'Inline | #PCDATA | p'; - $address->content_model_type = 'optional'; - $address->child = false; - - $blockquote = $this->addBlankElement('blockquote'); - $blockquote->content_model = 'Flow | #PCDATA'; - $blockquote->content_model_type = 'optional'; - $blockquote->child = false; - - $br = $this->addBlankElement('br'); - $br->attr['clear'] = 'Enum#left,all,right,none'; - - $caption = $this->addBlankElement('caption'); - $caption->attr['align'] = 'Enum#top,bottom,left,right'; - - $div = $this->addBlankElement('div'); - $div->attr['align'] = $align; - - $dl = $this->addBlankElement('dl'); - $dl->attr['compact'] = 'Bool#compact'; - - for ($i = 1; $i <= 6; $i++) { - $h = $this->addBlankElement("h$i"); - $h->attr['align'] = $align; - } - - $hr = $this->addBlankElement('hr'); - $hr->attr['align'] = $align; - $hr->attr['noshade'] = 'Bool#noshade'; - $hr->attr['size'] = 'Pixels'; - $hr->attr['width'] = 'Length'; - - $img = $this->addBlankElement('img'); - $img->attr['align'] = 'IAlign'; - $img->attr['border'] = 'Pixels'; - $img->attr['hspace'] = 'Pixels'; - $img->attr['vspace'] = 'Pixels'; - - // figure out this integer business - - $li = $this->addBlankElement('li'); - $li->attr['value'] = new HTMLPurifier_AttrDef_Integer(); - $li->attr['type'] = 'Enum#s:1,i,I,a,A,disc,square,circle'; - - $ol = $this->addBlankElement('ol'); - $ol->attr['compact'] = 'Bool#compact'; - $ol->attr['start'] = new HTMLPurifier_AttrDef_Integer(); - $ol->attr['type'] = 'Enum#s:1,i,I,a,A'; - - $p = $this->addBlankElement('p'); - $p->attr['align'] = $align; - - $pre = $this->addBlankElement('pre'); - $pre->attr['width'] = 'Number'; - - // script omitted - - $table = $this->addBlankElement('table'); - $table->attr['align'] = 'Enum#left,center,right'; - $table->attr['bgcolor'] = 'Color'; - - $tr = $this->addBlankElement('tr'); - $tr->attr['bgcolor'] = 'Color'; - - $th = $this->addBlankElement('th'); - $th->attr['bgcolor'] = 'Color'; - $th->attr['height'] = 'Length'; - $th->attr['nowrap'] = 'Bool#nowrap'; - $th->attr['width'] = 'Length'; - - $td = $this->addBlankElement('td'); - $td->attr['bgcolor'] = 'Color'; - $td->attr['height'] = 'Length'; - $td->attr['nowrap'] = 'Bool#nowrap'; - $td->attr['width'] = 'Length'; - - $ul = $this->addBlankElement('ul'); - $ul->attr['compact'] = 'Bool#compact'; - $ul->attr['type'] = 'Enum#square,disc,circle'; - - // "safe" modifications to "unsafe" elements - // WARNING: If you want to add support for an unsafe, legacy - // attribute, make a new TrustedLegacy module with the trusted - // bit set appropriately - - $form = $this->addBlankElement('form'); - $form->content_model = 'Flow | #PCDATA'; - $form->content_model_type = 'optional'; - $form->attr['target'] = 'FrameTarget'; - - $input = $this->addBlankElement('input'); - $input->attr['align'] = 'IAlign'; - - $legend = $this->addBlankElement('legend'); - $legend->attr['align'] = 'LAlign'; - } -} - -// vim: et sw=4 sts=4 +addElement( + 'basefont', + 'Inline', + 'Empty', + null, + array( + 'color' => 'Color', + 'face' => 'Text', // extremely broad, we should + 'size' => 'Text', // tighten it + 'id' => 'ID' + ) + ); + $this->addElement('center', 'Block', 'Flow', 'Common'); + $this->addElement( + 'dir', + 'Block', + 'Required: li', + 'Common', + array( + 'compact' => 'Bool#compact' + ) + ); + $this->addElement( + 'font', + 'Inline', + 'Inline', + array('Core', 'I18N'), + array( + 'color' => 'Color', + 'face' => 'Text', // extremely broad, we should + 'size' => 'Text', // tighten it + ) + ); + $this->addElement( + 'menu', + 'Block', + 'Required: li', + 'Common', + array( + 'compact' => 'Bool#compact' + ) + ); + + $s = $this->addElement('s', 'Inline', 'Inline', 'Common'); + $s->formatting = true; + + $strike = $this->addElement('strike', 'Inline', 'Inline', 'Common'); + $strike->formatting = true; + + $u = $this->addElement('u', 'Inline', 'Inline', 'Common'); + $u->formatting = true; + + // setup modifications to old elements + + $align = 'Enum#left,right,center,justify'; + + $address = $this->addBlankElement('address'); + $address->content_model = 'Inline | #PCDATA | p'; + $address->content_model_type = 'optional'; + $address->child = false; + + $blockquote = $this->addBlankElement('blockquote'); + $blockquote->content_model = 'Flow | #PCDATA'; + $blockquote->content_model_type = 'optional'; + $blockquote->child = false; + + $br = $this->addBlankElement('br'); + $br->attr['clear'] = 'Enum#left,all,right,none'; + + $caption = $this->addBlankElement('caption'); + $caption->attr['align'] = 'Enum#top,bottom,left,right'; + + $div = $this->addBlankElement('div'); + $div->attr['align'] = $align; + + $dl = $this->addBlankElement('dl'); + $dl->attr['compact'] = 'Bool#compact'; + + for ($i = 1; $i <= 6; $i++) { + $h = $this->addBlankElement("h$i"); + $h->attr['align'] = $align; + } + + $hr = $this->addBlankElement('hr'); + $hr->attr['align'] = $align; + $hr->attr['noshade'] = 'Bool#noshade'; + $hr->attr['size'] = 'Pixels'; + $hr->attr['width'] = 'Length'; + + $img = $this->addBlankElement('img'); + $img->attr['align'] = 'IAlign'; + $img->attr['border'] = 'Pixels'; + $img->attr['hspace'] = 'Pixels'; + $img->attr['vspace'] = 'Pixels'; + + // figure out this integer business + + $li = $this->addBlankElement('li'); + $li->attr['value'] = new HTMLPurifier_AttrDef_Integer(); + $li->attr['type'] = 'Enum#s:1,i,I,a,A,disc,square,circle'; + + $ol = $this->addBlankElement('ol'); + $ol->attr['compact'] = 'Bool#compact'; + $ol->attr['start'] = new HTMLPurifier_AttrDef_Integer(); + $ol->attr['type'] = 'Enum#s:1,i,I,a,A'; + + $p = $this->addBlankElement('p'); + $p->attr['align'] = $align; + + $pre = $this->addBlankElement('pre'); + $pre->attr['width'] = 'Number'; + + // script omitted + + $table = $this->addBlankElement('table'); + $table->attr['align'] = 'Enum#left,center,right'; + $table->attr['bgcolor'] = 'Color'; + + $tr = $this->addBlankElement('tr'); + $tr->attr['bgcolor'] = 'Color'; + + $th = $this->addBlankElement('th'); + $th->attr['bgcolor'] = 'Color'; + $th->attr['height'] = 'Length'; + $th->attr['nowrap'] = 'Bool#nowrap'; + $th->attr['width'] = 'Length'; + + $td = $this->addBlankElement('td'); + $td->attr['bgcolor'] = 'Color'; + $td->attr['height'] = 'Length'; + $td->attr['nowrap'] = 'Bool#nowrap'; + $td->attr['width'] = 'Length'; + + $ul = $this->addBlankElement('ul'); + $ul->attr['compact'] = 'Bool#compact'; + $ul->attr['type'] = 'Enum#square,disc,circle'; + + // "safe" modifications to "unsafe" elements + // WARNING: If you want to add support for an unsafe, legacy + // attribute, make a new TrustedLegacy module with the trusted + // bit set appropriately + + $form = $this->addBlankElement('form'); + $form->content_model = 'Flow | #PCDATA'; + $form->content_model_type = 'optional'; + $form->attr['target'] = 'FrameTarget'; + + $input = $this->addBlankElement('input'); + $input->attr['align'] = 'IAlign'; + + $legend = $this->addBlankElement('legend'); + $legend->attr['align'] = 'LAlign'; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/HTMLModule/List.php b/lib/purifier/HTMLPurifier/HTMLModule/List.php index 605e37c97a..7a20ff701c 100644 --- a/lib/purifier/HTMLPurifier/HTMLModule/List.php +++ b/lib/purifier/HTMLPurifier/HTMLModule/List.php @@ -1,51 +1,51 @@ - 'List'); - - /** - * @param HTMLPurifier_Config $config - */ - public function setup($config) - { - $ol = $this->addElement('ol', 'List', new HTMLPurifier_ChildDef_List(), 'Common'); - $ul = $this->addElement('ul', 'List', new HTMLPurifier_ChildDef_List(), 'Common'); - // XXX The wrap attribute is handled by MakeWellFormed. This is all - // quite unsatisfactory, because we generated this - // *specifically* for lists, and now a big chunk of the handling - // is done properly by the List ChildDef. So actually, we just - // want enough information to make autoclosing work properly, - // and then hand off the tricky stuff to the ChildDef. - $ol->wrap = 'li'; - $ul->wrap = 'li'; - $this->addElement('dl', 'List', 'Required: dt | dd', 'Common'); - - $this->addElement('li', false, 'Flow', 'Common'); - - $this->addElement('dd', false, 'Flow', 'Common'); - $this->addElement('dt', false, 'Inline', 'Common'); - } -} - -// vim: et sw=4 sts=4 + 'List'); + + /** + * @param HTMLPurifier_Config $config + */ + public function setup($config) + { + $ol = $this->addElement('ol', 'List', new HTMLPurifier_ChildDef_List(), 'Common'); + $ul = $this->addElement('ul', 'List', new HTMLPurifier_ChildDef_List(), 'Common'); + // XXX The wrap attribute is handled by MakeWellFormed. This is all + // quite unsatisfactory, because we generated this + // *specifically* for lists, and now a big chunk of the handling + // is done properly by the List ChildDef. So actually, we just + // want enough information to make autoclosing work properly, + // and then hand off the tricky stuff to the ChildDef. + $ol->wrap = 'li'; + $ul->wrap = 'li'; + $this->addElement('dl', 'List', 'Required: dt | dd', 'Common'); + + $this->addElement('li', false, 'Flow', 'Common'); + + $this->addElement('dd', false, 'Flow', 'Common'); + $this->addElement('dt', false, 'Inline', 'Common'); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/HTMLModule/Name.php b/lib/purifier/HTMLPurifier/HTMLModule/Name.php index 315e22a803..60c0545154 100644 --- a/lib/purifier/HTMLPurifier/HTMLModule/Name.php +++ b/lib/purifier/HTMLPurifier/HTMLModule/Name.php @@ -1,26 +1,26 @@ -addBlankElement($name); - $element->attr['name'] = 'CDATA'; - if (!$config->get('HTML.Attr.Name.UseCDATA')) { - $element->attr_transform_post[] = new HTMLPurifier_AttrTransform_NameSync(); - } - } - } -} - -// vim: et sw=4 sts=4 +addBlankElement($name); + $element->attr['name'] = 'CDATA'; + if (!$config->get('HTML.Attr.Name.UseCDATA')) { + $element->attr_transform_post[] = new HTMLPurifier_AttrTransform_NameSync(); + } + } + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/HTMLModule/Nofollow.php b/lib/purifier/HTMLPurifier/HTMLModule/Nofollow.php index c145e8e969..dc9410a895 100644 --- a/lib/purifier/HTMLPurifier/HTMLModule/Nofollow.php +++ b/lib/purifier/HTMLPurifier/HTMLModule/Nofollow.php @@ -1,25 +1,25 @@ -addBlankElement('a'); - $a->attr_transform_post[] = new HTMLPurifier_AttrTransform_Nofollow(); - } -} - -// vim: et sw=4 sts=4 +addBlankElement('a'); + $a->attr_transform_post[] = new HTMLPurifier_AttrTransform_Nofollow(); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php b/lib/purifier/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php index 7d66e114f2..da722253ac 100644 --- a/lib/purifier/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php +++ b/lib/purifier/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php @@ -1,20 +1,20 @@ - array( - 'lang' => 'LanguageCode', - ) - ); -} - -// vim: et sw=4 sts=4 + array( + 'lang' => 'LanguageCode', + ) + ); +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/HTMLModule/Object.php b/lib/purifier/HTMLPurifier/HTMLModule/Object.php index d388b24c7f..2f9efc5c88 100644 --- a/lib/purifier/HTMLPurifier/HTMLModule/Object.php +++ b/lib/purifier/HTMLPurifier/HTMLModule/Object.php @@ -1,62 +1,62 @@ - to cater to legacy browsers: this - * module does not allow this sort of behavior - */ -class HTMLPurifier_HTMLModule_Object extends HTMLPurifier_HTMLModule -{ - /** - * @type string - */ - public $name = 'Object'; - - /** - * @type bool - */ - public $safe = false; - - /** - * @param HTMLPurifier_Config $config - */ - public function setup($config) - { - $this->addElement( - 'object', - 'Inline', - 'Optional: #PCDATA | Flow | param', - 'Common', - array( - 'archive' => 'URI', - 'classid' => 'URI', - 'codebase' => 'URI', - 'codetype' => 'Text', - 'data' => 'URI', - 'declare' => 'Bool#declare', - 'height' => 'Length', - 'name' => 'CDATA', - 'standby' => 'Text', - 'tabindex' => 'Number', - 'type' => 'ContentType', - 'width' => 'Length' - ) - ); - - $this->addElement( - 'param', - false, - 'Empty', - null, - array( - 'id' => 'ID', - 'name*' => 'Text', - 'type' => 'Text', - 'value' => 'Text', - 'valuetype' => 'Enum#data,ref,object' - ) - ); - } -} - -// vim: et sw=4 sts=4 + to cater to legacy browsers: this + * module does not allow this sort of behavior + */ +class HTMLPurifier_HTMLModule_Object extends HTMLPurifier_HTMLModule +{ + /** + * @type string + */ + public $name = 'Object'; + + /** + * @type bool + */ + public $safe = false; + + /** + * @param HTMLPurifier_Config $config + */ + public function setup($config) + { + $this->addElement( + 'object', + 'Inline', + 'Optional: #PCDATA | Flow | param', + 'Common', + array( + 'archive' => 'URI', + 'classid' => 'URI', + 'codebase' => 'URI', + 'codetype' => 'Text', + 'data' => 'URI', + 'declare' => 'Bool#declare', + 'height' => 'Length', + 'name' => 'CDATA', + 'standby' => 'Text', + 'tabindex' => 'Number', + 'type' => 'ContentType', + 'width' => 'Length' + ) + ); + + $this->addElement( + 'param', + false, + 'Empty', + null, + array( + 'id' => 'ID', + 'name*' => 'Text', + 'type' => 'Text', + 'value' => 'Text', + 'valuetype' => 'Enum#data,ref,object' + ) + ); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/HTMLModule/Presentation.php b/lib/purifier/HTMLPurifier/HTMLModule/Presentation.php index 831db4cfff..6458ce9d88 100644 --- a/lib/purifier/HTMLPurifier/HTMLModule/Presentation.php +++ b/lib/purifier/HTMLPurifier/HTMLModule/Presentation.php @@ -1,42 +1,42 @@ -addElement('hr', 'Block', 'Empty', 'Common'); - $this->addElement('sub', 'Inline', 'Inline', 'Common'); - $this->addElement('sup', 'Inline', 'Inline', 'Common'); - $b = $this->addElement('b', 'Inline', 'Inline', 'Common'); - $b->formatting = true; - $big = $this->addElement('big', 'Inline', 'Inline', 'Common'); - $big->formatting = true; - $i = $this->addElement('i', 'Inline', 'Inline', 'Common'); - $i->formatting = true; - $small = $this->addElement('small', 'Inline', 'Inline', 'Common'); - $small->formatting = true; - $tt = $this->addElement('tt', 'Inline', 'Inline', 'Common'); - $tt->formatting = true; - } -} - -// vim: et sw=4 sts=4 +addElement('hr', 'Block', 'Empty', 'Common'); + $this->addElement('sub', 'Inline', 'Inline', 'Common'); + $this->addElement('sup', 'Inline', 'Inline', 'Common'); + $b = $this->addElement('b', 'Inline', 'Inline', 'Common'); + $b->formatting = true; + $big = $this->addElement('big', 'Inline', 'Inline', 'Common'); + $big->formatting = true; + $i = $this->addElement('i', 'Inline', 'Inline', 'Common'); + $i->formatting = true; + $small = $this->addElement('small', 'Inline', 'Inline', 'Common'); + $small->formatting = true; + $tt = $this->addElement('tt', 'Inline', 'Inline', 'Common'); + $tt->formatting = true; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/HTMLModule/Proprietary.php b/lib/purifier/HTMLPurifier/HTMLModule/Proprietary.php index 4593fc40f3..5ee3c8e67f 100644 --- a/lib/purifier/HTMLPurifier/HTMLModule/Proprietary.php +++ b/lib/purifier/HTMLPurifier/HTMLModule/Proprietary.php @@ -1,40 +1,40 @@ -addElement( - 'marquee', - 'Inline', - 'Flow', - 'Common', - array( - 'direction' => 'Enum#left,right,up,down', - 'behavior' => 'Enum#alternate', - 'width' => 'Length', - 'height' => 'Length', - 'scrolldelay' => 'Number', - 'scrollamount' => 'Number', - 'loop' => 'Number', - 'bgcolor' => 'Color', - 'hspace' => 'Pixels', - 'vspace' => 'Pixels', - ) - ); - } -} - -// vim: et sw=4 sts=4 +addElement( + 'marquee', + 'Inline', + 'Flow', + 'Common', + array( + 'direction' => 'Enum#left,right,up,down', + 'behavior' => 'Enum#alternate', + 'width' => 'Length', + 'height' => 'Length', + 'scrolldelay' => 'Number', + 'scrollamount' => 'Number', + 'loop' => 'Number', + 'bgcolor' => 'Color', + 'hspace' => 'Pixels', + 'vspace' => 'Pixels', + ) + ); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/HTMLModule/Ruby.php b/lib/purifier/HTMLPurifier/HTMLModule/Ruby.php index 9a2617296a..a0d48924da 100644 --- a/lib/purifier/HTMLPurifier/HTMLModule/Ruby.php +++ b/lib/purifier/HTMLPurifier/HTMLModule/Ruby.php @@ -1,36 +1,36 @@ -addElement( - 'ruby', - 'Inline', - 'Custom: ((rb, (rt | (rp, rt, rp))) | (rbc, rtc, rtc?))', - 'Common' - ); - $this->addElement('rbc', false, 'Required: rb', 'Common'); - $this->addElement('rtc', false, 'Required: rt', 'Common'); - $rb = $this->addElement('rb', false, 'Inline', 'Common'); - $rb->excludes = array('ruby' => true); - $rt = $this->addElement('rt', false, 'Inline', 'Common', array('rbspan' => 'Number')); - $rt->excludes = array('ruby' => true); - $this->addElement('rp', false, 'Optional: #PCDATA', 'Common'); - } -} - -// vim: et sw=4 sts=4 +addElement( + 'ruby', + 'Inline', + 'Custom: ((rb, (rt | (rp, rt, rp))) | (rbc, rtc, rtc?))', + 'Common' + ); + $this->addElement('rbc', false, 'Required: rb', 'Common'); + $this->addElement('rtc', false, 'Required: rt', 'Common'); + $rb = $this->addElement('rb', false, 'Inline', 'Common'); + $rb->excludes = array('ruby' => true); + $rt = $this->addElement('rt', false, 'Inline', 'Common', array('rbspan' => 'Number')); + $rt->excludes = array('ruby' => true); + $this->addElement('rp', false, 'Optional: #PCDATA', 'Common'); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/HTMLModule/SafeEmbed.php b/lib/purifier/HTMLPurifier/HTMLModule/SafeEmbed.php index 11572887da..04e6689ea6 100644 --- a/lib/purifier/HTMLPurifier/HTMLModule/SafeEmbed.php +++ b/lib/purifier/HTMLPurifier/HTMLModule/SafeEmbed.php @@ -1,40 +1,40 @@ -get('HTML.MaxImgLength'); - $embed = $this->addElement( - 'embed', - 'Inline', - 'Empty', - 'Common', - array( - 'src*' => 'URI#embedded', - 'type' => 'Enum#application/x-shockwave-flash', - 'width' => 'Pixels#' . $max, - 'height' => 'Pixels#' . $max, - 'allowscriptaccess' => 'Enum#never', - 'allownetworking' => 'Enum#internal', - 'flashvars' => 'Text', - 'wmode' => 'Enum#window,transparent,opaque', - 'name' => 'ID', - ) - ); - $embed->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeEmbed(); - } -} - -// vim: et sw=4 sts=4 +get('HTML.MaxImgLength'); + $embed = $this->addElement( + 'embed', + 'Inline', + 'Empty', + 'Common', + array( + 'src*' => 'URI#embedded', + 'type' => 'Enum#application/x-shockwave-flash', + 'width' => 'Pixels#' . $max, + 'height' => 'Pixels#' . $max, + 'allowscriptaccess' => 'Enum#never', + 'allownetworking' => 'Enum#internal', + 'flashvars' => 'Text', + 'wmode' => 'Enum#window,transparent,opaque', + 'name' => 'ID', + ) + ); + $embed->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeEmbed(); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/HTMLModule/SafeObject.php b/lib/purifier/HTMLPurifier/HTMLModule/SafeObject.php index a061cec152..1297f80a39 100644 --- a/lib/purifier/HTMLPurifier/HTMLModule/SafeObject.php +++ b/lib/purifier/HTMLPurifier/HTMLModule/SafeObject.php @@ -1,62 +1,62 @@ -get('HTML.MaxImgLength'); - $object = $this->addElement( - 'object', - 'Inline', - 'Optional: param | Flow | #PCDATA', - 'Common', - array( - // While technically not required by the spec, we're forcing - // it to this value. - 'type' => 'Enum#application/x-shockwave-flash', - 'width' => 'Pixels#' . $max, - 'height' => 'Pixels#' . $max, - 'data' => 'URI#embedded', - 'codebase' => new HTMLPurifier_AttrDef_Enum( - array( - 'http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0' - ) - ), - ) - ); - $object->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeObject(); - - $param = $this->addElement( - 'param', - false, - 'Empty', - false, - array( - 'id' => 'ID', - 'name*' => 'Text', - 'value' => 'Text' - ) - ); - $param->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeParam(); - $this->info_injector[] = 'SafeObject'; - } -} - -// vim: et sw=4 sts=4 +get('HTML.MaxImgLength'); + $object = $this->addElement( + 'object', + 'Inline', + 'Optional: param | Flow | #PCDATA', + 'Common', + array( + // While technically not required by the spec, we're forcing + // it to this value. + 'type' => 'Enum#application/x-shockwave-flash', + 'width' => 'Pixels#' . $max, + 'height' => 'Pixels#' . $max, + 'data' => 'URI#embedded', + 'codebase' => new HTMLPurifier_AttrDef_Enum( + array( + 'http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0' + ) + ), + ) + ); + $object->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeObject(); + + $param = $this->addElement( + 'param', + false, + 'Empty', + false, + array( + 'id' => 'ID', + 'name*' => 'Text', + 'value' => 'Text' + ) + ); + $param->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeParam(); + $this->info_injector[] = 'SafeObject'; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/HTMLModule/SafeScripting.php b/lib/purifier/HTMLPurifier/HTMLModule/SafeScripting.php index 6e9113cb59..aea7584c37 100644 --- a/lib/purifier/HTMLPurifier/HTMLModule/SafeScripting.php +++ b/lib/purifier/HTMLPurifier/HTMLModule/SafeScripting.php @@ -1,40 +1,40 @@ -get('HTML.SafeScripting'); - $script = $this->addElement( - 'script', - 'Inline', - 'Empty', - null, - array( - // While technically not required by the spec, we're forcing - // it to this value. - 'type' => 'Enum#text/javascript', - 'src*' => new HTMLPurifier_AttrDef_Enum(array_keys($allowed)) - ) - ); - $script->attr_transform_pre[] = - $script->attr_transform_post[] = new HTMLPurifier_AttrTransform_ScriptRequired(); - } -} - -// vim: et sw=4 sts=4 +get('HTML.SafeScripting'); + $script = $this->addElement( + 'script', + 'Inline', + 'Optional:', // Not `Empty` to not allow to autoclose the #i', '', $html); - } - - return $html; - } - - /** - * Takes a string of HTML (fragment or document) and returns the content - * @todo Consider making protected - */ - public function extractBody($html) - { - $matches = array(); - $result = preg_match('|(.*?)]*>(.*)|is', $html, $matches); - if ($result) { - // Make sure it's not in a comment - $comment_start = strrpos($matches[1], ''); - if ($comment_start === false || - ($comment_end !== false && $comment_end > $comment_start)) { - return $matches[2]; - } - } - return $html; - } -} - -// vim: et sw=4 sts=4 +get('Core.LexerImpl'); + } + + $needs_tracking = + $config->get('Core.MaintainLineNumbers') || + $config->get('Core.CollectErrors'); + + $inst = null; + if (is_object($lexer)) { + $inst = $lexer; + } else { + if (is_null($lexer)) { + do { + // auto-detection algorithm + if ($needs_tracking) { + $lexer = 'DirectLex'; + break; + } + + if (class_exists('DOMDocument', false) && + method_exists('DOMDocument', 'loadHTML') && + !extension_loaded('domxml') + ) { + // check for DOM support, because while it's part of the + // core, it can be disabled compile time. Also, the PECL + // domxml extension overrides the default DOM, and is evil + // and nasty and we shan't bother to support it + $lexer = 'DOMLex'; + } else { + $lexer = 'DirectLex'; + } + } while (0); + } // do..while so we can break + + // instantiate recognized string names + switch ($lexer) { + case 'DOMLex': + $inst = new HTMLPurifier_Lexer_DOMLex(); + break; + case 'DirectLex': + $inst = new HTMLPurifier_Lexer_DirectLex(); + break; + case 'PH5P': + $inst = new HTMLPurifier_Lexer_PH5P(); + break; + default: + throw new HTMLPurifier_Exception( + "Cannot instantiate unrecognized Lexer type " . + htmlspecialchars($lexer) + ); + } + } + + if (!$inst) { + throw new HTMLPurifier_Exception('No lexer was instantiated'); + } + + // once PHP DOM implements native line numbers, or we + // hack out something using XSLT, remove this stipulation + if ($needs_tracking && !$inst->tracksLineNumbers) { + throw new HTMLPurifier_Exception( + 'Cannot use lexer that does not support line numbers with ' . + 'Core.MaintainLineNumbers or Core.CollectErrors (use DirectLex instead)' + ); + } + + return $inst; + + } + + // -- CONVENIENCE MEMBERS --------------------------------------------- + + public function __construct() + { + $this->_entity_parser = new HTMLPurifier_EntityParser(); + } + + /** + * Most common entity to raw value conversion table for special entities. + * @type array + */ + protected $_special_entity2str = + array( + '"' => '"', + '&' => '&', + '<' => '<', + '>' => '>', + ''' => "'", + ''' => "'", + ''' => "'" + ); + + public function parseText($string, $config) { + return $this->parseData($string, false, $config); + } + + public function parseAttr($string, $config) { + return $this->parseData($string, true, $config); + } + + /** + * Parses special entities into the proper characters. + * + * This string will translate escaped versions of the special characters + * into the correct ones. + * + * @param string $string String character data to be parsed. + * @return string Parsed character data. + */ + public function parseData($string, $is_attr, $config) + { + // following functions require at least one character + if ($string === '') { + return ''; + } + + // subtracts amps that cannot possibly be escaped + $num_amp = substr_count($string, '&') - substr_count($string, '& ') - + ($string[strlen($string) - 1] === '&' ? 1 : 0); + + if (!$num_amp) { + return $string; + } // abort if no entities + $num_esc_amp = substr_count($string, '&'); + $string = strtr($string, $this->_special_entity2str); + + // code duplication for sake of optimization, see above + $num_amp_2 = substr_count($string, '&') - substr_count($string, '& ') - + ($string[strlen($string) - 1] === '&' ? 1 : 0); + + if ($num_amp_2 <= $num_esc_amp) { + return $string; + } + + // hmm... now we have some uncommon entities. Use the callback. + if ($config->get('Core.LegacyEntityDecoder')) { + $string = $this->_entity_parser->substituteSpecialEntities($string); + } else { + if ($is_attr) { + $string = $this->_entity_parser->substituteAttrEntities($string); + } else { + $string = $this->_entity_parser->substituteTextEntities($string); + } + } + return $string; + } + + /** + * Lexes an HTML string into tokens. + * @param $string String HTML. + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return HTMLPurifier_Token[] array representation of HTML. + */ + public function tokenizeHTML($string, $config, $context) + { + trigger_error('Call to abstract class', E_USER_ERROR); + } + + /** + * Translates CDATA sections into regular sections (through escaping). + * @param string $string HTML string to process. + * @return string HTML with CDATA sections escaped. + */ + protected static function escapeCDATA($string) + { + return preg_replace_callback( + '//s', + array('HTMLPurifier_Lexer', 'CDATACallback'), + $string + ); + } + + /** + * Special CDATA case that is especially convoluted for #i', '', $html); + } + + return $html; + } + + /** + * Takes a string of HTML (fragment or document) and returns the content + * @todo Consider making protected + */ + public function extractBody($html) + { + $matches = array(); + $result = preg_match('|(.*?)]*>(.*)|is', $html, $matches); + if ($result) { + // Make sure it's not in a comment + $comment_start = strrpos($matches[1], ''); + if ($comment_start === false || + ($comment_end !== false && $comment_end > $comment_start)) { + return $matches[2]; + } + } + return $html; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/Lexer/DOMLex.php b/lib/purifier/HTMLPurifier/Lexer/DOMLex.php index b3ccf4832a..ca5f25b849 100644 --- a/lib/purifier/HTMLPurifier/Lexer/DOMLex.php +++ b/lib/purifier/HTMLPurifier/Lexer/DOMLex.php @@ -1,291 +1,338 @@ -factory = new HTMLPurifier_TokenFactory(); - } - - /** - * @param string $html - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return HTMLPurifier_Token[] - */ - public function tokenizeHTML($html, $config, $context) - { - $html = $this->normalize($html, $config, $context); - - // attempt to armor stray angled brackets that cannot possibly - // form tags and thus are probably being used as emoticons - if ($config->get('Core.AggressivelyFixLt')) { - $char = '[^a-z!\/]'; - $comment = "/|\z)/is"; - $html = preg_replace_callback($comment, array($this, 'callbackArmorCommentEntities'), $html); - do { - $old = $html; - $html = preg_replace("/<($char)/i", '<\\1', $html); - } while ($html !== $old); - $html = preg_replace_callback($comment, array($this, 'callbackUndoCommentSubst'), $html); // fix comments - } - - // preprocess html, essential for UTF-8 - $html = $this->wrapHTML($html, $config, $context); - - $doc = new DOMDocument(); - $doc->encoding = 'UTF-8'; // theoretically, the above has this covered - - set_error_handler(array($this, 'muteErrorHandler')); - $doc->loadHTML($html); - restore_error_handler(); - - $body = $doc->getElementsByTagName('html')->item(0)-> // - getElementsByTagName('body')->item(0); // - - $div = $body->getElementsByTagName('div')->item(0); //
- $tokens = array(); - $this->tokenizeDOM($div, $tokens, $config); - // If the div has a sibling, that means we tripped across - // a premature
tag. So remove the div we parsed, - // and then tokenize the rest of body. We can't tokenize - // the sibling directly as we'll lose the tags in that case. - if ($div->nextSibling) { - $body->removeChild($div); - $this->tokenizeDOM($body, $tokens, $config); - } - return $tokens; - } - - /** - * Iterative function that tokenizes a node, putting it into an accumulator. - * To iterate is human, to recurse divine - L. Peter Deutsch - * @param DOMNode $node DOMNode to be tokenized. - * @param HTMLPurifier_Token[] $tokens Array-list of already tokenized tokens. - * @return HTMLPurifier_Token of node appended to previously passed tokens. - */ - protected function tokenizeDOM($node, &$tokens, $config) - { - $level = 0; - $nodes = array($level => new HTMLPurifier_Queue(array($node))); - $closingNodes = array(); - do { - while (!$nodes[$level]->isEmpty()) { - $node = $nodes[$level]->shift(); // FIFO - $collect = $level > 0 ? true : false; - $needEndingTag = $this->createStartNode($node, $tokens, $collect, $config); - if ($needEndingTag) { - $closingNodes[$level][] = $node; - } - if ($node->childNodes && $node->childNodes->length) { - $level++; - $nodes[$level] = new HTMLPurifier_Queue(); - foreach ($node->childNodes as $childNode) { - $nodes[$level]->push($childNode); - } - } - } - $level--; - if ($level && isset($closingNodes[$level])) { - while ($node = array_pop($closingNodes[$level])) { - $this->createEndNode($node, $tokens); - } - } - } while ($level > 0); - } - - /** - * @param DOMNode $node DOMNode to be tokenized. - * @param HTMLPurifier_Token[] $tokens Array-list of already tokenized tokens. - * @param bool $collect Says whether or start and close are collected, set to - * false at first recursion because it's the implicit DIV - * tag you're dealing with. - * @return bool if the token needs an endtoken - * @todo data and tagName properties don't seem to exist in DOMNode? - */ - protected function createStartNode($node, &$tokens, $collect, $config) - { - // intercept non element nodes. WE MUST catch all of them, - // but we're not getting the character reference nodes because - // those should have been preprocessed - if ($node->nodeType === XML_TEXT_NODE) { - $tokens[] = $this->factory->createText($node->data); - return false; - } elseif ($node->nodeType === XML_CDATA_SECTION_NODE) { - // undo libxml's special treatment of )#si', - array($this, 'scriptCallback'), - $html - ); - } - - $html = $this->normalize($html, $config, $context); - - $cursor = 0; // our location in the text - $inside_tag = false; // whether or not we're parsing the inside of a tag - $array = array(); // result array - - // This is also treated to mean maintain *column* numbers too - $maintain_line_numbers = $config->get('Core.MaintainLineNumbers'); - - if ($maintain_line_numbers === null) { - // automatically determine line numbering by checking - // if error collection is on - $maintain_line_numbers = $config->get('Core.CollectErrors'); - } - - if ($maintain_line_numbers) { - $current_line = 1; - $current_col = 0; - $length = strlen($html); - } else { - $current_line = false; - $current_col = false; - $length = false; - } - $context->register('CurrentLine', $current_line); - $context->register('CurrentCol', $current_col); - $nl = "\n"; - // how often to manually recalculate. This will ALWAYS be right, - // but it's pretty wasteful. Set to 0 to turn off - $synchronize_interval = $config->get('Core.DirectLexLineNumberSyncInterval'); - - $e = false; - if ($config->get('Core.CollectErrors')) { - $e =& $context->get('ErrorCollector'); - } - - // for testing synchronization - $loops = 0; - - while (++$loops) { - // $cursor is either at the start of a token, or inside of - // a tag (i.e. there was a < immediately before it), as indicated - // by $inside_tag - - if ($maintain_line_numbers) { - // $rcursor, however, is always at the start of a token. - $rcursor = $cursor - (int)$inside_tag; - - // Column number is cheap, so we calculate it every round. - // We're interested at the *end* of the newline string, so - // we need to add strlen($nl) == 1 to $nl_pos before subtracting it - // from our "rcursor" position. - $nl_pos = strrpos($html, $nl, $rcursor - $length); - $current_col = $rcursor - (is_bool($nl_pos) ? 0 : $nl_pos + 1); - - // recalculate lines - if ($synchronize_interval && // synchronization is on - $cursor > 0 && // cursor is further than zero - $loops % $synchronize_interval === 0) { // time to synchronize! - $current_line = 1 + $this->substrCount($html, $nl, 0, $cursor); - } - } - - $position_next_lt = strpos($html, '<', $cursor); - $position_next_gt = strpos($html, '>', $cursor); - - // triggers on "asdf" but not "asdf " - // special case to set up context - if ($position_next_lt === $cursor) { - $inside_tag = true; - $cursor++; - } - - if (!$inside_tag && $position_next_lt !== false) { - // We are not inside tag and there still is another tag to parse - $token = new - HTMLPurifier_Token_Text( - $this->parseText( - substr( - $html, - $cursor, - $position_next_lt - $cursor - ), $config - ) - ); - if ($maintain_line_numbers) { - $token->rawPosition($current_line, $current_col); - $current_line += $this->substrCount($html, $nl, $cursor, $position_next_lt - $cursor); - } - $array[] = $token; - $cursor = $position_next_lt + 1; - $inside_tag = true; - continue; - } elseif (!$inside_tag) { - // We are not inside tag but there are no more tags - // If we're already at the end, break - if ($cursor === strlen($html)) { - break; - } - // Create Text of rest of string - $token = new - HTMLPurifier_Token_Text( - $this->parseText( - substr( - $html, - $cursor - ), $config - ) - ); - if ($maintain_line_numbers) { - $token->rawPosition($current_line, $current_col); - } - $array[] = $token; - break; - } elseif ($inside_tag && $position_next_gt !== false) { - // We are in tag and it is well formed - // Grab the internals of the tag - $strlen_segment = $position_next_gt - $cursor; - - if ($strlen_segment < 1) { - // there's nothing to process! - $token = new HTMLPurifier_Token_Text('<'); - $cursor++; - continue; - } - - $segment = substr($html, $cursor, $strlen_segment); - - if ($segment === false) { - // somehow, we attempted to access beyond the end of - // the string, defense-in-depth, reported by Nate Abele - break; - } - - // Check if it's a comment - if (substr($segment, 0, 3) === '!--') { - // re-determine segment length, looking for --> - $position_comment_end = strpos($html, '-->', $cursor); - if ($position_comment_end === false) { - // uh oh, we have a comment that extends to - // infinity. Can't be helped: set comment - // end position to end of string - if ($e) { - $e->send(E_WARNING, 'Lexer: Unclosed comment'); - } - $position_comment_end = strlen($html); - $end = true; - } else { - $end = false; - } - $strlen_segment = $position_comment_end - $cursor; - $segment = substr($html, $cursor, $strlen_segment); - $token = new - HTMLPurifier_Token_Comment( - substr( - $segment, - 3, - $strlen_segment - 3 - ) - ); - if ($maintain_line_numbers) { - $token->rawPosition($current_line, $current_col); - $current_line += $this->substrCount($html, $nl, $cursor, $strlen_segment); - } - $array[] = $token; - $cursor = $end ? $position_comment_end : $position_comment_end + 3; - $inside_tag = false; - continue; - } - - // Check if it's an end tag - $is_end_tag = (strpos($segment, '/') === 0); - if ($is_end_tag) { - $type = substr($segment, 1); - $token = new HTMLPurifier_Token_End($type); - if ($maintain_line_numbers) { - $token->rawPosition($current_line, $current_col); - $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); - } - $array[] = $token; - $inside_tag = false; - $cursor = $position_next_gt + 1; - continue; - } - - // Check leading character is alnum, if not, we may - // have accidently grabbed an emoticon. Translate into - // text and go our merry way - if (!ctype_alpha($segment[0])) { - // XML: $segment[0] !== '_' && $segment[0] !== ':' - if ($e) { - $e->send(E_NOTICE, 'Lexer: Unescaped lt'); - } - $token = new HTMLPurifier_Token_Text('<'); - if ($maintain_line_numbers) { - $token->rawPosition($current_line, $current_col); - $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); - } - $array[] = $token; - $inside_tag = false; - continue; - } - - // Check if it is explicitly self closing, if so, remove - // trailing slash. Remember, we could have a tag like
, so - // any later token processing scripts must convert improperly - // classified EmptyTags from StartTags. - $is_self_closing = (strrpos($segment, '/') === $strlen_segment - 1); - if ($is_self_closing) { - $strlen_segment--; - $segment = substr($segment, 0, $strlen_segment); - } - - // Check if there are any attributes - $position_first_space = strcspn($segment, $this->_whitespace); - - if ($position_first_space >= $strlen_segment) { - if ($is_self_closing) { - $token = new HTMLPurifier_Token_Empty($segment); - } else { - $token = new HTMLPurifier_Token_Start($segment); - } - if ($maintain_line_numbers) { - $token->rawPosition($current_line, $current_col); - $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); - } - $array[] = $token; - $inside_tag = false; - $cursor = $position_next_gt + 1; - continue; - } - - // Grab out all the data - $type = substr($segment, 0, $position_first_space); - $attribute_string = - trim( - substr( - $segment, - $position_first_space - ) - ); - if ($attribute_string) { - $attr = $this->parseAttributeString( - $attribute_string, - $config, - $context - ); - } else { - $attr = array(); - } - - if ($is_self_closing) { - $token = new HTMLPurifier_Token_Empty($type, $attr); - } else { - $token = new HTMLPurifier_Token_Start($type, $attr); - } - if ($maintain_line_numbers) { - $token->rawPosition($current_line, $current_col); - $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); - } - $array[] = $token; - $cursor = $position_next_gt + 1; - $inside_tag = false; - continue; - } else { - // inside tag, but there's no ending > sign - if ($e) { - $e->send(E_WARNING, 'Lexer: Missing gt'); - } - $token = new - HTMLPurifier_Token_Text( - '<' . - $this->parseText( - substr($html, $cursor), $config - ) - ); - if ($maintain_line_numbers) { - $token->rawPosition($current_line, $current_col); - } - // no cursor scroll? Hmm... - $array[] = $token; - break; - } - break; - } - - $context->destroy('CurrentLine'); - $context->destroy('CurrentCol'); - return $array; - } - - /** - * PHP 5.0.x compatible substr_count that implements offset and length - * @param string $haystack - * @param string $needle - * @param int $offset - * @param int $length - * @return int - */ - protected function substrCount($haystack, $needle, $offset, $length) - { - static $oldVersion; - if ($oldVersion === null) { - $oldVersion = version_compare(PHP_VERSION, '5.1', '<'); - } - if ($oldVersion) { - $haystack = substr($haystack, $offset, $length); - return substr_count($haystack, $needle); - } else { - return substr_count($haystack, $needle, $offset, $length); - } - } - - /** - * Takes the inside of an HTML tag and makes an assoc array of attributes. - * - * @param string $string Inside of tag excluding name. - * @param HTMLPurifier_Config $config - * @param HTMLPurifier_Context $context - * @return array Assoc array of attributes. - */ - public function parseAttributeString($string, $config, $context) - { - $string = (string)$string; // quick typecast - - if ($string == '') { - return array(); - } // no attributes - - $e = false; - if ($config->get('Core.CollectErrors')) { - $e =& $context->get('ErrorCollector'); - } - - // let's see if we can abort as quickly as possible - // one equal sign, no spaces => one attribute - $num_equal = substr_count($string, '='); - $has_space = strpos($string, ' '); - if ($num_equal === 0 && !$has_space) { - // bool attribute - return array($string => $string); - } elseif ($num_equal === 1 && !$has_space) { - // only one attribute - list($key, $quoted_value) = explode('=', $string); - $quoted_value = trim($quoted_value); - if (!$key) { - if ($e) { - $e->send(E_ERROR, 'Lexer: Missing attribute key'); - } - return array(); - } - if (!$quoted_value) { - return array($key => ''); - } - $first_char = @$quoted_value[0]; - $last_char = @$quoted_value[strlen($quoted_value) - 1]; - - $same_quote = ($first_char == $last_char); - $open_quote = ($first_char == '"' || $first_char == "'"); - - if ($same_quote && $open_quote) { - // well behaved - $value = substr($quoted_value, 1, strlen($quoted_value) - 2); - } else { - // not well behaved - if ($open_quote) { - if ($e) { - $e->send(E_ERROR, 'Lexer: Missing end quote'); - } - $value = substr($quoted_value, 1); - } else { - $value = $quoted_value; - } - } - if ($value === false) { - $value = ''; - } - return array($key => $this->parseAttr($value, $config)); - } - - // setup loop environment - $array = array(); // return assoc array of attributes - $cursor = 0; // current position in string (moves forward) - $size = strlen($string); // size of the string (stays the same) - - // if we have unquoted attributes, the parser expects a terminating - // space, so let's guarantee that there's always a terminating space. - $string .= ' '; - - $old_cursor = -1; - while ($cursor < $size) { - if ($old_cursor >= $cursor) { - throw new Exception("Infinite loop detected"); - } - $old_cursor = $cursor; - - $cursor += ($value = strspn($string, $this->_whitespace, $cursor)); - // grab the key - - $key_begin = $cursor; //we're currently at the start of the key - - // scroll past all characters that are the key (not whitespace or =) - $cursor += strcspn($string, $this->_whitespace . '=', $cursor); - - $key_end = $cursor; // now at the end of the key - - $key = substr($string, $key_begin, $key_end - $key_begin); - - if (!$key) { - if ($e) { - $e->send(E_ERROR, 'Lexer: Missing attribute key'); - } - $cursor += 1 + strcspn($string, $this->_whitespace, $cursor + 1); // prevent infinite loop - continue; // empty key - } - - // scroll past all whitespace - $cursor += strspn($string, $this->_whitespace, $cursor); - - if ($cursor >= $size) { - $array[$key] = $key; - break; - } - - // if the next character is an equal sign, we've got a regular - // pair, otherwise, it's a bool attribute - $first_char = @$string[$cursor]; - - if ($first_char == '=') { - // key="value" - - $cursor++; - $cursor += strspn($string, $this->_whitespace, $cursor); - - if ($cursor === false) { - $array[$key] = ''; - break; - } - - // we might be in front of a quote right now - - $char = @$string[$cursor]; - - if ($char == '"' || $char == "'") { - // it's quoted, end bound is $char - $cursor++; - $value_begin = $cursor; - $cursor = strpos($string, $char, $cursor); - $value_end = $cursor; - } else { - // it's not quoted, end bound is whitespace - $value_begin = $cursor; - $cursor += strcspn($string, $this->_whitespace, $cursor); - $value_end = $cursor; - } - - // we reached a premature end - if ($cursor === false) { - $cursor = $size; - $value_end = $cursor; - } - - $value = substr($string, $value_begin, $value_end - $value_begin); - if ($value === false) { - $value = ''; - } - $array[$key] = $this->parseAttr($value, $config); - $cursor++; - } else { - // boolattr - if ($key !== '') { - $array[$key] = $key; - } else { - // purely theoretical - if ($e) { - $e->send(E_ERROR, 'Lexer: Missing attribute key'); - } - } - } - } - return $array; - } -} - -// vim: et sw=4 sts=4 +get('HTML.Trusted')) { + $html = preg_replace_callback( + '#(]*>)(\s*[^<].+?)()#si', + array($this, 'scriptCallback'), + $html + ); + } + + $html = $this->normalize($html, $config, $context); + + $cursor = 0; // our location in the text + $inside_tag = false; // whether or not we're parsing the inside of a tag + $array = array(); // result array + + // This is also treated to mean maintain *column* numbers too + $maintain_line_numbers = $config->get('Core.MaintainLineNumbers'); + + if ($maintain_line_numbers === null) { + // automatically determine line numbering by checking + // if error collection is on + $maintain_line_numbers = $config->get('Core.CollectErrors'); + } + + if ($maintain_line_numbers) { + $current_line = 1; + $current_col = 0; + $length = strlen($html); + } else { + $current_line = false; + $current_col = false; + $length = false; + } + $context->register('CurrentLine', $current_line); + $context->register('CurrentCol', $current_col); + $nl = "\n"; + // how often to manually recalculate. This will ALWAYS be right, + // but it's pretty wasteful. Set to 0 to turn off + $synchronize_interval = $config->get('Core.DirectLexLineNumberSyncInterval'); + + $e = false; + if ($config->get('Core.CollectErrors')) { + $e =& $context->get('ErrorCollector'); + } + + // for testing synchronization + $loops = 0; + + while (++$loops) { + // $cursor is either at the start of a token, or inside of + // a tag (i.e. there was a < immediately before it), as indicated + // by $inside_tag + + if ($maintain_line_numbers) { + // $rcursor, however, is always at the start of a token. + $rcursor = $cursor - (int)$inside_tag; + + // Column number is cheap, so we calculate it every round. + // We're interested at the *end* of the newline string, so + // we need to add strlen($nl) == 1 to $nl_pos before subtracting it + // from our "rcursor" position. + $nl_pos = strrpos($html, $nl, $rcursor - $length); + $current_col = $rcursor - (is_bool($nl_pos) ? 0 : $nl_pos + 1); + + // recalculate lines + if ($synchronize_interval && // synchronization is on + $cursor > 0 && // cursor is further than zero + $loops % $synchronize_interval === 0) { // time to synchronize! + $current_line = 1 + $this->substrCount($html, $nl, 0, $cursor); + } + } + + $position_next_lt = strpos($html, '<', $cursor); + $position_next_gt = strpos($html, '>', $cursor); + + // triggers on "asdf" but not "asdf " + // special case to set up context + if ($position_next_lt === $cursor) { + $inside_tag = true; + $cursor++; + } + + if (!$inside_tag && $position_next_lt !== false) { + // We are not inside tag and there still is another tag to parse + $token = new + HTMLPurifier_Token_Text( + $this->parseText( + substr( + $html, + $cursor, + $position_next_lt - $cursor + ), $config + ) + ); + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + $current_line += $this->substrCount($html, $nl, $cursor, $position_next_lt - $cursor); + } + $array[] = $token; + $cursor = $position_next_lt + 1; + $inside_tag = true; + continue; + } elseif (!$inside_tag) { + // We are not inside tag but there are no more tags + // If we're already at the end, break + if ($cursor === strlen($html)) { + break; + } + // Create Text of rest of string + $token = new + HTMLPurifier_Token_Text( + $this->parseText( + substr( + $html, + $cursor + ), $config + ) + ); + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + } + $array[] = $token; + break; + } elseif ($inside_tag && $position_next_gt !== false) { + // We are in tag and it is well formed + // Grab the internals of the tag + $strlen_segment = $position_next_gt - $cursor; + + if ($strlen_segment < 1) { + // there's nothing to process! + $token = new HTMLPurifier_Token_Text('<'); + $cursor++; + continue; + } + + $segment = substr($html, $cursor, $strlen_segment); + + if ($segment === false) { + // somehow, we attempted to access beyond the end of + // the string, defense-in-depth, reported by Nate Abele + break; + } + + // Check if it's a comment + if (substr($segment, 0, 3) === '!--') { + // re-determine segment length, looking for --> + $position_comment_end = strpos($html, '-->', $cursor); + if ($position_comment_end === false) { + // uh oh, we have a comment that extends to + // infinity. Can't be helped: set comment + // end position to end of string + if ($e) { + $e->send(E_WARNING, 'Lexer: Unclosed comment'); + } + $position_comment_end = strlen($html); + $end = true; + } else { + $end = false; + } + $strlen_segment = $position_comment_end - $cursor; + $segment = substr($html, $cursor, $strlen_segment); + $token = new + HTMLPurifier_Token_Comment( + substr( + $segment, + 3, + $strlen_segment - 3 + ) + ); + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + $current_line += $this->substrCount($html, $nl, $cursor, $strlen_segment); + } + $array[] = $token; + $cursor = $end ? $position_comment_end : $position_comment_end + 3; + $inside_tag = false; + continue; + } + + // Check if it's an end tag + $is_end_tag = (strpos($segment, '/') === 0); + if ($is_end_tag) { + $type = substr($segment, 1); + $token = new HTMLPurifier_Token_End($type); + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); + } + $array[] = $token; + $inside_tag = false; + $cursor = $position_next_gt + 1; + continue; + } + + // Check leading character is alnum, if not, we may + // have accidently grabbed an emoticon. Translate into + // text and go our merry way + if (!ctype_alpha($segment[0])) { + // XML: $segment[0] !== '_' && $segment[0] !== ':' + if ($e) { + $e->send(E_NOTICE, 'Lexer: Unescaped lt'); + } + $token = new HTMLPurifier_Token_Text('<'); + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); + } + $array[] = $token; + $inside_tag = false; + continue; + } + + // Check if it is explicitly self closing, if so, remove + // trailing slash. Remember, we could have a tag like
, so + // any later token processing scripts must convert improperly + // classified EmptyTags from StartTags. + $is_self_closing = (strrpos($segment, '/') === $strlen_segment - 1); + if ($is_self_closing) { + $strlen_segment--; + $segment = substr($segment, 0, $strlen_segment); + } + + // Check if there are any attributes + $position_first_space = strcspn($segment, $this->_whitespace); + + if ($position_first_space >= $strlen_segment) { + if ($is_self_closing) { + $token = new HTMLPurifier_Token_Empty($segment); + } else { + $token = new HTMLPurifier_Token_Start($segment); + } + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); + } + $array[] = $token; + $inside_tag = false; + $cursor = $position_next_gt + 1; + continue; + } + + // Grab out all the data + $type = substr($segment, 0, $position_first_space); + $attribute_string = + trim( + substr( + $segment, + $position_first_space + ) + ); + if ($attribute_string) { + $attr = $this->parseAttributeString( + $attribute_string, + $config, + $context + ); + } else { + $attr = array(); + } + + if ($is_self_closing) { + $token = new HTMLPurifier_Token_Empty($type, $attr); + } else { + $token = new HTMLPurifier_Token_Start($type, $attr); + } + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); + } + $array[] = $token; + $cursor = $position_next_gt + 1; + $inside_tag = false; + continue; + } else { + // inside tag, but there's no ending > sign + if ($e) { + $e->send(E_WARNING, 'Lexer: Missing gt'); + } + $token = new + HTMLPurifier_Token_Text( + '<' . + $this->parseText( + substr($html, $cursor), $config + ) + ); + if ($maintain_line_numbers) { + $token->rawPosition($current_line, $current_col); + } + // no cursor scroll? Hmm... + $array[] = $token; + break; + } + break; + } + + $context->destroy('CurrentLine'); + $context->destroy('CurrentCol'); + return $array; + } + + /** + * PHP 5.0.x compatible substr_count that implements offset and length + * @param string $haystack + * @param string $needle + * @param int $offset + * @param int $length + * @return int + */ + protected function substrCount($haystack, $needle, $offset, $length) + { + static $oldVersion; + if ($oldVersion === null) { + $oldVersion = version_compare(PHP_VERSION, '5.1', '<'); + } + if ($oldVersion) { + $haystack = substr($haystack, $offset, $length); + return substr_count($haystack, $needle); + } else { + return substr_count($haystack, $needle, $offset, $length); + } + } + + /** + * Takes the inside of an HTML tag and makes an assoc array of attributes. + * + * @param string $string Inside of tag excluding name. + * @param HTMLPurifier_Config $config + * @param HTMLPurifier_Context $context + * @return array Assoc array of attributes. + */ + public function parseAttributeString($string, $config, $context) + { + $string = (string)$string; // quick typecast + + if ($string == '') { + return array(); + } // no attributes + + $e = false; + if ($config->get('Core.CollectErrors')) { + $e =& $context->get('ErrorCollector'); + } + + // let's see if we can abort as quickly as possible + // one equal sign, no spaces => one attribute + $num_equal = substr_count($string, '='); + $has_space = strpos($string, ' '); + if ($num_equal === 0 && !$has_space) { + // bool attribute + return array($string => $string); + } elseif ($num_equal === 1 && !$has_space) { + // only one attribute + list($key, $quoted_value) = explode('=', $string); + $quoted_value = trim($quoted_value); + if (!$key) { + if ($e) { + $e->send(E_ERROR, 'Lexer: Missing attribute key'); + } + return array(); + } + if (!$quoted_value) { + return array($key => ''); + } + $first_char = @$quoted_value[0]; + $last_char = @$quoted_value[strlen($quoted_value) - 1]; + + $same_quote = ($first_char == $last_char); + $open_quote = ($first_char == '"' || $first_char == "'"); + + if ($same_quote && $open_quote) { + // well behaved + $value = substr($quoted_value, 1, strlen($quoted_value) - 2); + } else { + // not well behaved + if ($open_quote) { + if ($e) { + $e->send(E_ERROR, 'Lexer: Missing end quote'); + } + $value = substr($quoted_value, 1); + } else { + $value = $quoted_value; + } + } + if ($value === false) { + $value = ''; + } + return array($key => $this->parseAttr($value, $config)); + } + + // setup loop environment + $array = array(); // return assoc array of attributes + $cursor = 0; // current position in string (moves forward) + $size = strlen($string); // size of the string (stays the same) + + // if we have unquoted attributes, the parser expects a terminating + // space, so let's guarantee that there's always a terminating space. + $string .= ' '; + + $old_cursor = -1; + while ($cursor < $size) { + if ($old_cursor >= $cursor) { + throw new Exception("Infinite loop detected"); + } + $old_cursor = $cursor; + + $cursor += ($value = strspn($string, $this->_whitespace, $cursor)); + // grab the key + + $key_begin = $cursor; //we're currently at the start of the key + + // scroll past all characters that are the key (not whitespace or =) + $cursor += strcspn($string, $this->_whitespace . '=', $cursor); + + $key_end = $cursor; // now at the end of the key + + $key = substr($string, $key_begin, $key_end - $key_begin); + + if (!$key) { + if ($e) { + $e->send(E_ERROR, 'Lexer: Missing attribute key'); + } + $cursor += 1 + strcspn($string, $this->_whitespace, $cursor + 1); // prevent infinite loop + continue; // empty key + } + + // scroll past all whitespace + $cursor += strspn($string, $this->_whitespace, $cursor); + + if ($cursor >= $size) { + $array[$key] = $key; + break; + } + + // if the next character is an equal sign, we've got a regular + // pair, otherwise, it's a bool attribute + $first_char = @$string[$cursor]; + + if ($first_char == '=') { + // key="value" + + $cursor++; + $cursor += strspn($string, $this->_whitespace, $cursor); + + if ($cursor === false) { + $array[$key] = ''; + break; + } + + // we might be in front of a quote right now + + $char = @$string[$cursor]; + + if ($char == '"' || $char == "'") { + // it's quoted, end bound is $char + $cursor++; + $value_begin = $cursor; + $cursor = strpos($string, $char, $cursor); + $value_end = $cursor; + } else { + // it's not quoted, end bound is whitespace + $value_begin = $cursor; + $cursor += strcspn($string, $this->_whitespace, $cursor); + $value_end = $cursor; + } + + // we reached a premature end + if ($cursor === false) { + $cursor = $size; + $value_end = $cursor; + } + + $value = substr($string, $value_begin, $value_end - $value_begin); + if ($value === false) { + $value = ''; + } + $array[$key] = $this->parseAttr($value, $config); + $cursor++; + } else { + // boolattr + if ($key !== '') { + $array[$key] = $key; + } else { + // purely theoretical + if ($e) { + $e->send(E_ERROR, 'Lexer: Missing attribute key'); + } + } + } + } + return $array; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/Lexer/PH5P.php b/lib/purifier/HTMLPurifier/Lexer/PH5P.php index b084ee9656..72476ddf32 100644 --- a/lib/purifier/HTMLPurifier/Lexer/PH5P.php +++ b/lib/purifier/HTMLPurifier/Lexer/PH5P.php @@ -1,4788 +1,4788 @@ -normalize($html, $config, $context); - $new_html = $this->wrapHTML($new_html, $config, $context, false /* no div */); - try { - $parser = new HTML5($new_html); - $doc = $parser->save(); - } catch (DOMException $e) { - // Uh oh, it failed. Punt to DirectLex. - $lexer = new HTMLPurifier_Lexer_DirectLex(); - $context->register('PH5PError', $e); // save the error, so we can detect it - return $lexer->tokenizeHTML($html, $config, $context); // use original HTML - } - $tokens = array(); - $this->tokenizeDOM( - $doc->getElementsByTagName('html')->item(0)-> // - getElementsByTagName('body')->item(0) // - , - $tokens, $config - ); - return $tokens; - } -} - -/* - -Copyright 2007 Jeroen van der Meer - -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. - -*/ - -class HTML5 -{ - private $data; - private $char; - private $EOF; - private $state; - private $tree; - private $token; - private $content_model; - private $escape = false; - private $entities = array( - 'AElig;', - 'AElig', - 'AMP;', - 'AMP', - 'Aacute;', - 'Aacute', - 'Acirc;', - 'Acirc', - 'Agrave;', - 'Agrave', - 'Alpha;', - 'Aring;', - 'Aring', - 'Atilde;', - 'Atilde', - 'Auml;', - 'Auml', - 'Beta;', - 'COPY;', - 'COPY', - 'Ccedil;', - 'Ccedil', - 'Chi;', - 'Dagger;', - 'Delta;', - 'ETH;', - 'ETH', - 'Eacute;', - 'Eacute', - 'Ecirc;', - 'Ecirc', - 'Egrave;', - 'Egrave', - 'Epsilon;', - 'Eta;', - 'Euml;', - 'Euml', - 'GT;', - 'GT', - 'Gamma;', - 'Iacute;', - 'Iacute', - 'Icirc;', - 'Icirc', - 'Igrave;', - 'Igrave', - 'Iota;', - 'Iuml;', - 'Iuml', - 'Kappa;', - 'LT;', - 'LT', - 'Lambda;', - 'Mu;', - 'Ntilde;', - 'Ntilde', - 'Nu;', - 'OElig;', - 'Oacute;', - 'Oacute', - 'Ocirc;', - 'Ocirc', - 'Ograve;', - 'Ograve', - 'Omega;', - 'Omicron;', - 'Oslash;', - 'Oslash', - 'Otilde;', - 'Otilde', - 'Ouml;', - 'Ouml', - 'Phi;', - 'Pi;', - 'Prime;', - 'Psi;', - 'QUOT;', - 'QUOT', - 'REG;', - 'REG', - 'Rho;', - 'Scaron;', - 'Sigma;', - 'THORN;', - 'THORN', - 'TRADE;', - 'Tau;', - 'Theta;', - 'Uacute;', - 'Uacute', - 'Ucirc;', - 'Ucirc', - 'Ugrave;', - 'Ugrave', - 'Upsilon;', - 'Uuml;', - 'Uuml', - 'Xi;', - 'Yacute;', - 'Yacute', - 'Yuml;', - 'Zeta;', - 'aacute;', - 'aacute', - 'acirc;', - 'acirc', - 'acute;', - 'acute', - 'aelig;', - 'aelig', - 'agrave;', - 'agrave', - 'alefsym;', - 'alpha;', - 'amp;', - 'amp', - 'and;', - 'ang;', - 'apos;', - 'aring;', - 'aring', - 'asymp;', - 'atilde;', - 'atilde', - 'auml;', - 'auml', - 'bdquo;', - 'beta;', - 'brvbar;', - 'brvbar', - 'bull;', - 'cap;', - 'ccedil;', - 'ccedil', - 'cedil;', - 'cedil', - 'cent;', - 'cent', - 'chi;', - 'circ;', - 'clubs;', - 'cong;', - 'copy;', - 'copy', - 'crarr;', - 'cup;', - 'curren;', - 'curren', - 'dArr;', - 'dagger;', - 'darr;', - 'deg;', - 'deg', - 'delta;', - 'diams;', - 'divide;', - 'divide', - 'eacute;', - 'eacute', - 'ecirc;', - 'ecirc', - 'egrave;', - 'egrave', - 'empty;', - 'emsp;', - 'ensp;', - 'epsilon;', - 'equiv;', - 'eta;', - 'eth;', - 'eth', - 'euml;', - 'euml', - 'euro;', - 'exist;', - 'fnof;', - 'forall;', - 'frac12;', - 'frac12', - 'frac14;', - 'frac14', - 'frac34;', - 'frac34', - 'frasl;', - 'gamma;', - 'ge;', - 'gt;', - 'gt', - 'hArr;', - 'harr;', - 'hearts;', - 'hellip;', - 'iacute;', - 'iacute', - 'icirc;', - 'icirc', - 'iexcl;', - 'iexcl', - 'igrave;', - 'igrave', - 'image;', - 'infin;', - 'int;', - 'iota;', - 'iquest;', - 'iquest', - 'isin;', - 'iuml;', - 'iuml', - 'kappa;', - 'lArr;', - 'lambda;', - 'lang;', - 'laquo;', - 'laquo', - 'larr;', - 'lceil;', - 'ldquo;', - 'le;', - 'lfloor;', - 'lowast;', - 'loz;', - 'lrm;', - 'lsaquo;', - 'lsquo;', - 'lt;', - 'lt', - 'macr;', - 'macr', - 'mdash;', - 'micro;', - 'micro', - 'middot;', - 'middot', - 'minus;', - 'mu;', - 'nabla;', - 'nbsp;', - 'nbsp', - 'ndash;', - 'ne;', - 'ni;', - 'not;', - 'not', - 'notin;', - 'nsub;', - 'ntilde;', - 'ntilde', - 'nu;', - 'oacute;', - 'oacute', - 'ocirc;', - 'ocirc', - 'oelig;', - 'ograve;', - 'ograve', - 'oline;', - 'omega;', - 'omicron;', - 'oplus;', - 'or;', - 'ordf;', - 'ordf', - 'ordm;', - 'ordm', - 'oslash;', - 'oslash', - 'otilde;', - 'otilde', - 'otimes;', - 'ouml;', - 'ouml', - 'para;', - 'para', - 'part;', - 'permil;', - 'perp;', - 'phi;', - 'pi;', - 'piv;', - 'plusmn;', - 'plusmn', - 'pound;', - 'pound', - 'prime;', - 'prod;', - 'prop;', - 'psi;', - 'quot;', - 'quot', - 'rArr;', - 'radic;', - 'rang;', - 'raquo;', - 'raquo', - 'rarr;', - 'rceil;', - 'rdquo;', - 'real;', - 'reg;', - 'reg', - 'rfloor;', - 'rho;', - 'rlm;', - 'rsaquo;', - 'rsquo;', - 'sbquo;', - 'scaron;', - 'sdot;', - 'sect;', - 'sect', - 'shy;', - 'shy', - 'sigma;', - 'sigmaf;', - 'sim;', - 'spades;', - 'sub;', - 'sube;', - 'sum;', - 'sup1;', - 'sup1', - 'sup2;', - 'sup2', - 'sup3;', - 'sup3', - 'sup;', - 'supe;', - 'szlig;', - 'szlig', - 'tau;', - 'there4;', - 'theta;', - 'thetasym;', - 'thinsp;', - 'thorn;', - 'thorn', - 'tilde;', - 'times;', - 'times', - 'trade;', - 'uArr;', - 'uacute;', - 'uacute', - 'uarr;', - 'ucirc;', - 'ucirc', - 'ugrave;', - 'ugrave', - 'uml;', - 'uml', - 'upsih;', - 'upsilon;', - 'uuml;', - 'uuml', - 'weierp;', - 'xi;', - 'yacute;', - 'yacute', - 'yen;', - 'yen', - 'yuml;', - 'yuml', - 'zeta;', - 'zwj;', - 'zwnj;' - ); - - const PCDATA = 0; - const RCDATA = 1; - const CDATA = 2; - const PLAINTEXT = 3; - - const DOCTYPE = 0; - const STARTTAG = 1; - const ENDTAG = 2; - const COMMENT = 3; - const CHARACTR = 4; - const EOF = 5; - - public function __construct($data) - { - $this->data = $data; - $this->char = -1; - $this->EOF = strlen($data); - $this->tree = new HTML5TreeConstructer; - $this->content_model = self::PCDATA; - - $this->state = 'data'; - - while ($this->state !== null) { - $this->{$this->state . 'State'}(); - } - } - - public function save() - { - return $this->tree->save(); - } - - private function char() - { - return ($this->char < $this->EOF) - ? $this->data[$this->char] - : false; - } - - private function character($s, $l = 0) - { - if ($s + $l < $this->EOF) { - if ($l === 0) { - return $this->data[$s]; - } else { - return substr($this->data, $s, $l); - } - } - } - - private function characters($char_class, $start) - { - return preg_replace('#^([' . $char_class . ']+).*#s', '\\1', substr($this->data, $start)); - } - - private function dataState() - { - // Consume the next input character - $this->char++; - $char = $this->char(); - - if ($char === '&' && ($this->content_model === self::PCDATA || $this->content_model === self::RCDATA)) { - /* U+0026 AMPERSAND (&) - When the content model flag is set to one of the PCDATA or RCDATA - states: switch to the entity data state. Otherwise: treat it as per - the "anything else" entry below. */ - $this->state = 'entityData'; - - } elseif ($char === '-') { - /* If the content model flag is set to either the RCDATA state or - the CDATA state, and the escape flag is false, and there are at - least three characters before this one in the input stream, and the - last four characters in the input stream, including this one, are - U+003C LESS-THAN SIGN, U+0021 EXCLAMATION MARK, U+002D HYPHEN-MINUS, - and U+002D HYPHEN-MINUS (""), - set the escape flag to false. */ - if (($this->content_model === self::RCDATA || - $this->content_model === self::CDATA) && $this->escape === true && - $this->character($this->char, 3) === '-->' - ) { - $this->escape = false; - } - - /* In any case, emit the input character as a character token. - Stay in the data state. */ - $this->emitToken( - array( - 'type' => self::CHARACTR, - 'data' => $char - ) - ); - - } elseif ($this->char === $this->EOF) { - /* EOF - Emit an end-of-file token. */ - $this->EOF(); - - } elseif ($this->content_model === self::PLAINTEXT) { - /* When the content model flag is set to the PLAINTEXT state - THIS DIFFERS GREATLY FROM THE SPEC: Get the remaining characters of - the text and emit it as a character token. */ - $this->emitToken( - array( - 'type' => self::CHARACTR, - 'data' => substr($this->data, $this->char) - ) - ); - - $this->EOF(); - - } else { - /* Anything else - THIS DIFFERS GREATLY FROM THE SPEC: Get as many character that - otherwise would also be treated as a character token and emit it - as a single character token. Stay in the data state. */ - $len = strcspn($this->data, '<&', $this->char); - $char = substr($this->data, $this->char, $len); - $this->char += $len - 1; - - $this->emitToken( - array( - 'type' => self::CHARACTR, - 'data' => $char - ) - ); - - $this->state = 'data'; - } - } - - private function entityDataState() - { - // Attempt to consume an entity. - $entity = $this->entity(); - - // If nothing is returned, emit a U+0026 AMPERSAND character token. - // Otherwise, emit the character token that was returned. - $char = (!$entity) ? '&' : $entity; - $this->emitToken( - array( - 'type' => self::CHARACTR, - 'data' => $char - ) - ); - - // Finally, switch to the data state. - $this->state = 'data'; - } - - private function tagOpenState() - { - switch ($this->content_model) { - case self::RCDATA: - case self::CDATA: - /* If the next input character is a U+002F SOLIDUS (/) character, - consume it and switch to the close tag open state. If the next - input character is not a U+002F SOLIDUS (/) character, emit a - U+003C LESS-THAN SIGN character token and switch to the data - state to process the next input character. */ - if ($this->character($this->char + 1) === '/') { - $this->char++; - $this->state = 'closeTagOpen'; - - } else { - $this->emitToken( - array( - 'type' => self::CHARACTR, - 'data' => '<' - ) - ); - - $this->state = 'data'; - } - break; - - case self::PCDATA: - // If the content model flag is set to the PCDATA state - // Consume the next input character: - $this->char++; - $char = $this->char(); - - if ($char === '!') { - /* U+0021 EXCLAMATION MARK (!) - Switch to the markup declaration open state. */ - $this->state = 'markupDeclarationOpen'; - - } elseif ($char === '/') { - /* U+002F SOLIDUS (/) - Switch to the close tag open state. */ - $this->state = 'closeTagOpen'; - - } elseif (preg_match('/^[A-Za-z]$/', $char)) { - /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z - Create a new start tag token, set its tag name to the lowercase - version of the input character (add 0x0020 to the character's code - point), then switch to the tag name state. (Don't emit the token - yet; further details will be filled in before it is emitted.) */ - $this->token = array( - 'name' => strtolower($char), - 'type' => self::STARTTAG, - 'attr' => array() - ); - - $this->state = 'tagName'; - - } elseif ($char === '>') { - /* U+003E GREATER-THAN SIGN (>) - Parse error. Emit a U+003C LESS-THAN SIGN character token and a - U+003E GREATER-THAN SIGN character token. Switch to the data state. */ - $this->emitToken( - array( - 'type' => self::CHARACTR, - 'data' => '<>' - ) - ); - - $this->state = 'data'; - - } elseif ($char === '?') { - /* U+003F QUESTION MARK (?) - Parse error. Switch to the bogus comment state. */ - $this->state = 'bogusComment'; - - } else { - /* Anything else - Parse error. Emit a U+003C LESS-THAN SIGN character token and - reconsume the current input character in the data state. */ - $this->emitToken( - array( - 'type' => self::CHARACTR, - 'data' => '<' - ) - ); - - $this->char--; - $this->state = 'data'; - } - break; - } - } - - private function closeTagOpenState() - { - $next_node = strtolower($this->characters('A-Za-z', $this->char + 1)); - $the_same = count($this->tree->stack) > 0 && $next_node === end($this->tree->stack)->nodeName; - - if (($this->content_model === self::RCDATA || $this->content_model === self::CDATA) && - (!$the_same || ($the_same && (!preg_match( - '/[\t\n\x0b\x0c >\/]/', - $this->character($this->char + 1 + strlen($next_node)) - ) || $this->EOF === $this->char))) - ) { - /* If the content model flag is set to the RCDATA or CDATA states then - examine the next few characters. If they do not match the tag name of - the last start tag token emitted (case insensitively), or if they do but - they are not immediately followed by one of the following characters: - * U+0009 CHARACTER TABULATION - * U+000A LINE FEED (LF) - * U+000B LINE TABULATION - * U+000C FORM FEED (FF) - * U+0020 SPACE - * U+003E GREATER-THAN SIGN (>) - * U+002F SOLIDUS (/) - * EOF - ...then there is a parse error. Emit a U+003C LESS-THAN SIGN character - token, a U+002F SOLIDUS character token, and switch to the data state - to process the next input character. */ - $this->emitToken( - array( - 'type' => self::CHARACTR, - 'data' => 'state = 'data'; - - } else { - /* Otherwise, if the content model flag is set to the PCDATA state, - or if the next few characters do match that tag name, consume the - next input character: */ - $this->char++; - $char = $this->char(); - - if (preg_match('/^[A-Za-z]$/', $char)) { - /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z - Create a new end tag token, set its tag name to the lowercase version - of the input character (add 0x0020 to the character's code point), then - switch to the tag name state. (Don't emit the token yet; further details - will be filled in before it is emitted.) */ - $this->token = array( - 'name' => strtolower($char), - 'type' => self::ENDTAG - ); - - $this->state = 'tagName'; - - } elseif ($char === '>') { - /* U+003E GREATER-THAN SIGN (>) - Parse error. Switch to the data state. */ - $this->state = 'data'; - - } elseif ($this->char === $this->EOF) { - /* EOF - Parse error. Emit a U+003C LESS-THAN SIGN character token and a U+002F - SOLIDUS character token. Reconsume the EOF character in the data state. */ - $this->emitToken( - array( - 'type' => self::CHARACTR, - 'data' => 'char--; - $this->state = 'data'; - - } else { - /* Parse error. Switch to the bogus comment state. */ - $this->state = 'bogusComment'; - } - } - } - - private function tagNameState() - { - // Consume the next input character: - $this->char++; - $char = $this->character($this->char); - - if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - /* U+0009 CHARACTER TABULATION - U+000A LINE FEED (LF) - U+000B LINE TABULATION - U+000C FORM FEED (FF) - U+0020 SPACE - Switch to the before attribute name state. */ - $this->state = 'beforeAttributeName'; - - } elseif ($char === '>') { - /* U+003E GREATER-THAN SIGN (>) - Emit the current tag token. Switch to the data state. */ - $this->emitToken($this->token); - $this->state = 'data'; - - } elseif ($this->char === $this->EOF) { - /* EOF - Parse error. Emit the current tag token. Reconsume the EOF - character in the data state. */ - $this->emitToken($this->token); - - $this->char--; - $this->state = 'data'; - - } elseif ($char === '/') { - /* U+002F SOLIDUS (/) - Parse error unless this is a permitted slash. Switch to the before - attribute name state. */ - $this->state = 'beforeAttributeName'; - - } else { - /* Anything else - Append the current input character to the current tag token's tag name. - Stay in the tag name state. */ - $this->token['name'] .= strtolower($char); - $this->state = 'tagName'; - } - } - - private function beforeAttributeNameState() - { - // Consume the next input character: - $this->char++; - $char = $this->character($this->char); - - if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - /* U+0009 CHARACTER TABULATION - U+000A LINE FEED (LF) - U+000B LINE TABULATION - U+000C FORM FEED (FF) - U+0020 SPACE - Stay in the before attribute name state. */ - $this->state = 'beforeAttributeName'; - - } elseif ($char === '>') { - /* U+003E GREATER-THAN SIGN (>) - Emit the current tag token. Switch to the data state. */ - $this->emitToken($this->token); - $this->state = 'data'; - - } elseif ($char === '/') { - /* U+002F SOLIDUS (/) - Parse error unless this is a permitted slash. Stay in the before - attribute name state. */ - $this->state = 'beforeAttributeName'; - - } elseif ($this->char === $this->EOF) { - /* EOF - Parse error. Emit the current tag token. Reconsume the EOF - character in the data state. */ - $this->emitToken($this->token); - - $this->char--; - $this->state = 'data'; - - } else { - /* Anything else - Start a new attribute in the current tag token. Set that attribute's - name to the current input character, and its value to the empty string. - Switch to the attribute name state. */ - $this->token['attr'][] = array( - 'name' => strtolower($char), - 'value' => null - ); - - $this->state = 'attributeName'; - } - } - - private function attributeNameState() - { - // Consume the next input character: - $this->char++; - $char = $this->character($this->char); - - if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - /* U+0009 CHARACTER TABULATION - U+000A LINE FEED (LF) - U+000B LINE TABULATION - U+000C FORM FEED (FF) - U+0020 SPACE - Stay in the before attribute name state. */ - $this->state = 'afterAttributeName'; - - } elseif ($char === '=') { - /* U+003D EQUALS SIGN (=) - Switch to the before attribute value state. */ - $this->state = 'beforeAttributeValue'; - - } elseif ($char === '>') { - /* U+003E GREATER-THAN SIGN (>) - Emit the current tag token. Switch to the data state. */ - $this->emitToken($this->token); - $this->state = 'data'; - - } elseif ($char === '/' && $this->character($this->char + 1) !== '>') { - /* U+002F SOLIDUS (/) - Parse error unless this is a permitted slash. Switch to the before - attribute name state. */ - $this->state = 'beforeAttributeName'; - - } elseif ($this->char === $this->EOF) { - /* EOF - Parse error. Emit the current tag token. Reconsume the EOF - character in the data state. */ - $this->emitToken($this->token); - - $this->char--; - $this->state = 'data'; - - } else { - /* Anything else - Append the current input character to the current attribute's name. - Stay in the attribute name state. */ - $last = count($this->token['attr']) - 1; - $this->token['attr'][$last]['name'] .= strtolower($char); - - $this->state = 'attributeName'; - } - } - - private function afterAttributeNameState() - { - // Consume the next input character: - $this->char++; - $char = $this->character($this->char); - - if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - /* U+0009 CHARACTER TABULATION - U+000A LINE FEED (LF) - U+000B LINE TABULATION - U+000C FORM FEED (FF) - U+0020 SPACE - Stay in the after attribute name state. */ - $this->state = 'afterAttributeName'; - - } elseif ($char === '=') { - /* U+003D EQUALS SIGN (=) - Switch to the before attribute value state. */ - $this->state = 'beforeAttributeValue'; - - } elseif ($char === '>') { - /* U+003E GREATER-THAN SIGN (>) - Emit the current tag token. Switch to the data state. */ - $this->emitToken($this->token); - $this->state = 'data'; - - } elseif ($char === '/' && $this->character($this->char + 1) !== '>') { - /* U+002F SOLIDUS (/) - Parse error unless this is a permitted slash. Switch to the - before attribute name state. */ - $this->state = 'beforeAttributeName'; - - } elseif ($this->char === $this->EOF) { - /* EOF - Parse error. Emit the current tag token. Reconsume the EOF - character in the data state. */ - $this->emitToken($this->token); - - $this->char--; - $this->state = 'data'; - - } else { - /* Anything else - Start a new attribute in the current tag token. Set that attribute's - name to the current input character, and its value to the empty string. - Switch to the attribute name state. */ - $this->token['attr'][] = array( - 'name' => strtolower($char), - 'value' => null - ); - - $this->state = 'attributeName'; - } - } - - private function beforeAttributeValueState() - { - // Consume the next input character: - $this->char++; - $char = $this->character($this->char); - - if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - /* U+0009 CHARACTER TABULATION - U+000A LINE FEED (LF) - U+000B LINE TABULATION - U+000C FORM FEED (FF) - U+0020 SPACE - Stay in the before attribute value state. */ - $this->state = 'beforeAttributeValue'; - - } elseif ($char === '"') { - /* U+0022 QUOTATION MARK (") - Switch to the attribute value (double-quoted) state. */ - $this->state = 'attributeValueDoubleQuoted'; - - } elseif ($char === '&') { - /* U+0026 AMPERSAND (&) - Switch to the attribute value (unquoted) state and reconsume - this input character. */ - $this->char--; - $this->state = 'attributeValueUnquoted'; - - } elseif ($char === '\'') { - /* U+0027 APOSTROPHE (') - Switch to the attribute value (single-quoted) state. */ - $this->state = 'attributeValueSingleQuoted'; - - } elseif ($char === '>') { - /* U+003E GREATER-THAN SIGN (>) - Emit the current tag token. Switch to the data state. */ - $this->emitToken($this->token); - $this->state = 'data'; - - } else { - /* Anything else - Append the current input character to the current attribute's value. - Switch to the attribute value (unquoted) state. */ - $last = count($this->token['attr']) - 1; - $this->token['attr'][$last]['value'] .= $char; - - $this->state = 'attributeValueUnquoted'; - } - } - - private function attributeValueDoubleQuotedState() - { - // Consume the next input character: - $this->char++; - $char = $this->character($this->char); - - if ($char === '"') { - /* U+0022 QUOTATION MARK (") - Switch to the before attribute name state. */ - $this->state = 'beforeAttributeName'; - - } elseif ($char === '&') { - /* U+0026 AMPERSAND (&) - Switch to the entity in attribute value state. */ - $this->entityInAttributeValueState('double'); - - } elseif ($this->char === $this->EOF) { - /* EOF - Parse error. Emit the current tag token. Reconsume the character - in the data state. */ - $this->emitToken($this->token); - - $this->char--; - $this->state = 'data'; - - } else { - /* Anything else - Append the current input character to the current attribute's value. - Stay in the attribute value (double-quoted) state. */ - $last = count($this->token['attr']) - 1; - $this->token['attr'][$last]['value'] .= $char; - - $this->state = 'attributeValueDoubleQuoted'; - } - } - - private function attributeValueSingleQuotedState() - { - // Consume the next input character: - $this->char++; - $char = $this->character($this->char); - - if ($char === '\'') { - /* U+0022 QUOTATION MARK (') - Switch to the before attribute name state. */ - $this->state = 'beforeAttributeName'; - - } elseif ($char === '&') { - /* U+0026 AMPERSAND (&) - Switch to the entity in attribute value state. */ - $this->entityInAttributeValueState('single'); - - } elseif ($this->char === $this->EOF) { - /* EOF - Parse error. Emit the current tag token. Reconsume the character - in the data state. */ - $this->emitToken($this->token); - - $this->char--; - $this->state = 'data'; - - } else { - /* Anything else - Append the current input character to the current attribute's value. - Stay in the attribute value (single-quoted) state. */ - $last = count($this->token['attr']) - 1; - $this->token['attr'][$last]['value'] .= $char; - - $this->state = 'attributeValueSingleQuoted'; - } - } - - private function attributeValueUnquotedState() - { - // Consume the next input character: - $this->char++; - $char = $this->character($this->char); - - if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - /* U+0009 CHARACTER TABULATION - U+000A LINE FEED (LF) - U+000B LINE TABULATION - U+000C FORM FEED (FF) - U+0020 SPACE - Switch to the before attribute name state. */ - $this->state = 'beforeAttributeName'; - - } elseif ($char === '&') { - /* U+0026 AMPERSAND (&) - Switch to the entity in attribute value state. */ - $this->entityInAttributeValueState(); - - } elseif ($char === '>') { - /* U+003E GREATER-THAN SIGN (>) - Emit the current tag token. Switch to the data state. */ - $this->emitToken($this->token); - $this->state = 'data'; - - } else { - /* Anything else - Append the current input character to the current attribute's value. - Stay in the attribute value (unquoted) state. */ - $last = count($this->token['attr']) - 1; - $this->token['attr'][$last]['value'] .= $char; - - $this->state = 'attributeValueUnquoted'; - } - } - - private function entityInAttributeValueState() - { - // Attempt to consume an entity. - $entity = $this->entity(); - - // If nothing is returned, append a U+0026 AMPERSAND character to the - // current attribute's value. Otherwise, emit the character token that - // was returned. - $char = (!$entity) - ? '&' - : $entity; - - $last = count($this->token['attr']) - 1; - $this->token['attr'][$last]['value'] .= $char; - } - - private function bogusCommentState() - { - /* Consume every character up to the first U+003E GREATER-THAN SIGN - character (>) or the end of the file (EOF), whichever comes first. Emit - a comment token whose data is the concatenation of all the characters - starting from and including the character that caused the state machine - to switch into the bogus comment state, up to and including the last - consumed character before the U+003E character, if any, or up to the - end of the file otherwise. (If the comment was started by the end of - the file (EOF), the token is empty.) */ - $data = $this->characters('^>', $this->char); - $this->emitToken( - array( - 'data' => $data, - 'type' => self::COMMENT - ) - ); - - $this->char += strlen($data); - - /* Switch to the data state. */ - $this->state = 'data'; - - /* If the end of the file was reached, reconsume the EOF character. */ - if ($this->char === $this->EOF) { - $this->char = $this->EOF - 1; - } - } - - private function markupDeclarationOpenState() - { - /* If the next two characters are both U+002D HYPHEN-MINUS (-) - characters, consume those two characters, create a comment token whose - data is the empty string, and switch to the comment state. */ - if ($this->character($this->char + 1, 2) === '--') { - $this->char += 2; - $this->state = 'comment'; - $this->token = array( - 'data' => null, - 'type' => self::COMMENT - ); - - /* Otherwise if the next seven chacacters are a case-insensitive match - for the word "DOCTYPE", then consume those characters and switch to the - DOCTYPE state. */ - } elseif (strtolower($this->character($this->char + 1, 7)) === 'doctype') { - $this->char += 7; - $this->state = 'doctype'; - - /* Otherwise, is is a parse error. Switch to the bogus comment state. - The next character that is consumed, if any, is the first character - that will be in the comment. */ - } else { - $this->char++; - $this->state = 'bogusComment'; - } - } - - private function commentState() - { - /* Consume the next input character: */ - $this->char++; - $char = $this->char(); - - /* U+002D HYPHEN-MINUS (-) */ - if ($char === '-') { - /* Switch to the comment dash state */ - $this->state = 'commentDash'; - - /* EOF */ - } elseif ($this->char === $this->EOF) { - /* Parse error. Emit the comment token. Reconsume the EOF character - in the data state. */ - $this->emitToken($this->token); - $this->char--; - $this->state = 'data'; - - /* Anything else */ - } else { - /* Append the input character to the comment token's data. Stay in - the comment state. */ - $this->token['data'] .= $char; - } - } - - private function commentDashState() - { - /* Consume the next input character: */ - $this->char++; - $char = $this->char(); - - /* U+002D HYPHEN-MINUS (-) */ - if ($char === '-') { - /* Switch to the comment end state */ - $this->state = 'commentEnd'; - - /* EOF */ - } elseif ($this->char === $this->EOF) { - /* Parse error. Emit the comment token. Reconsume the EOF character - in the data state. */ - $this->emitToken($this->token); - $this->char--; - $this->state = 'data'; - - /* Anything else */ - } else { - /* Append a U+002D HYPHEN-MINUS (-) character and the input - character to the comment token's data. Switch to the comment state. */ - $this->token['data'] .= '-' . $char; - $this->state = 'comment'; - } - } - - private function commentEndState() - { - /* Consume the next input character: */ - $this->char++; - $char = $this->char(); - - if ($char === '>') { - $this->emitToken($this->token); - $this->state = 'data'; - - } elseif ($char === '-') { - $this->token['data'] .= '-'; - - } elseif ($this->char === $this->EOF) { - $this->emitToken($this->token); - $this->char--; - $this->state = 'data'; - - } else { - $this->token['data'] .= '--' . $char; - $this->state = 'comment'; - } - } - - private function doctypeState() - { - /* Consume the next input character: */ - $this->char++; - $char = $this->char(); - - if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - $this->state = 'beforeDoctypeName'; - - } else { - $this->char--; - $this->state = 'beforeDoctypeName'; - } - } - - private function beforeDoctypeNameState() - { - /* Consume the next input character: */ - $this->char++; - $char = $this->char(); - - if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - // Stay in the before DOCTYPE name state. - - } elseif (preg_match('/^[a-z]$/', $char)) { - $this->token = array( - 'name' => strtoupper($char), - 'type' => self::DOCTYPE, - 'error' => true - ); - - $this->state = 'doctypeName'; - - } elseif ($char === '>') { - $this->emitToken( - array( - 'name' => null, - 'type' => self::DOCTYPE, - 'error' => true - ) - ); - - $this->state = 'data'; - - } elseif ($this->char === $this->EOF) { - $this->emitToken( - array( - 'name' => null, - 'type' => self::DOCTYPE, - 'error' => true - ) - ); - - $this->char--; - $this->state = 'data'; - - } else { - $this->token = array( - 'name' => $char, - 'type' => self::DOCTYPE, - 'error' => true - ); - - $this->state = 'doctypeName'; - } - } - - private function doctypeNameState() - { - /* Consume the next input character: */ - $this->char++; - $char = $this->char(); - - if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - $this->state = 'AfterDoctypeName'; - - } elseif ($char === '>') { - $this->emitToken($this->token); - $this->state = 'data'; - - } elseif (preg_match('/^[a-z]$/', $char)) { - $this->token['name'] .= strtoupper($char); - - } elseif ($this->char === $this->EOF) { - $this->emitToken($this->token); - $this->char--; - $this->state = 'data'; - - } else { - $this->token['name'] .= $char; - } - - $this->token['error'] = ($this->token['name'] === 'HTML') - ? false - : true; - } - - private function afterDoctypeNameState() - { - /* Consume the next input character: */ - $this->char++; - $char = $this->char(); - - if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - // Stay in the DOCTYPE name state. - - } elseif ($char === '>') { - $this->emitToken($this->token); - $this->state = 'data'; - - } elseif ($this->char === $this->EOF) { - $this->emitToken($this->token); - $this->char--; - $this->state = 'data'; - - } else { - $this->token['error'] = true; - $this->state = 'bogusDoctype'; - } - } - - private function bogusDoctypeState() - { - /* Consume the next input character: */ - $this->char++; - $char = $this->char(); - - if ($char === '>') { - $this->emitToken($this->token); - $this->state = 'data'; - - } elseif ($this->char === $this->EOF) { - $this->emitToken($this->token); - $this->char--; - $this->state = 'data'; - - } else { - // Stay in the bogus DOCTYPE state. - } - } - - private function entity() - { - $start = $this->char; - - // This section defines how to consume an entity. This definition is - // used when parsing entities in text and in attributes. - - // The behaviour depends on the identity of the next character (the - // one immediately after the U+0026 AMPERSAND character): - - switch ($this->character($this->char + 1)) { - // U+0023 NUMBER SIGN (#) - case '#': - - // The behaviour further depends on the character after the - // U+0023 NUMBER SIGN: - switch ($this->character($this->char + 1)) { - // U+0078 LATIN SMALL LETTER X - // U+0058 LATIN CAPITAL LETTER X - case 'x': - case 'X': - // Follow the steps below, but using the range of - // characters U+0030 DIGIT ZERO through to U+0039 DIGIT - // NINE, U+0061 LATIN SMALL LETTER A through to U+0066 - // LATIN SMALL LETTER F, and U+0041 LATIN CAPITAL LETTER - // A, through to U+0046 LATIN CAPITAL LETTER F (in other - // words, 0-9, A-F, a-f). - $char = 1; - $char_class = '0-9A-Fa-f'; - break; - - // Anything else - default: - // Follow the steps below, but using the range of - // characters U+0030 DIGIT ZERO through to U+0039 DIGIT - // NINE (i.e. just 0-9). - $char = 0; - $char_class = '0-9'; - break; - } - - // Consume as many characters as match the range of characters - // given above. - $this->char++; - $e_name = $this->characters($char_class, $this->char + $char + 1); - $entity = $this->character($start, $this->char); - $cond = strlen($e_name) > 0; - - // The rest of the parsing happens bellow. - break; - - // Anything else - default: - // Consume the maximum number of characters possible, with the - // consumed characters case-sensitively matching one of the - // identifiers in the first column of the entities table. - - $e_name = $this->characters('0-9A-Za-z;', $this->char + 1); - $len = strlen($e_name); - - for ($c = 1; $c <= $len; $c++) { - $id = substr($e_name, 0, $c); - $this->char++; - - if (in_array($id, $this->entities)) { - if ($e_name[$c - 1] !== ';') { - if ($c < $len && $e_name[$c] == ';') { - $this->char++; // consume extra semicolon - } - } - $entity = $id; - break; - } - } - - $cond = isset($entity); - // The rest of the parsing happens bellow. - break; - } - - if (!$cond) { - // If no match can be made, then this is a parse error. No - // characters are consumed, and nothing is returned. - $this->char = $start; - return false; - } - - // Return a character token for the character corresponding to the - // entity name (as given by the second column of the entities table). - return html_entity_decode('&' . rtrim($entity, ';') . ';', ENT_QUOTES, 'UTF-8'); - } - - private function emitToken($token) - { - $emit = $this->tree->emitToken($token); - - if (is_int($emit)) { - $this->content_model = $emit; - - } elseif ($token['type'] === self::ENDTAG) { - $this->content_model = self::PCDATA; - } - } - - private function EOF() - { - $this->state = null; - $this->tree->emitToken( - array( - 'type' => self::EOF - ) - ); - } -} - -class HTML5TreeConstructer -{ - public $stack = array(); - - private $phase; - private $mode; - private $dom; - private $foster_parent = null; - private $a_formatting = array(); - - private $head_pointer = null; - private $form_pointer = null; - - private $scoping = array('button', 'caption', 'html', 'marquee', 'object', 'table', 'td', 'th'); - private $formatting = array( - 'a', - 'b', - 'big', - 'em', - 'font', - 'i', - 'nobr', - 's', - 'small', - 'strike', - 'strong', - 'tt', - 'u' - ); - private $special = array( - 'address', - 'area', - 'base', - 'basefont', - 'bgsound', - 'blockquote', - 'body', - 'br', - 'center', - 'col', - 'colgroup', - 'dd', - 'dir', - 'div', - 'dl', - 'dt', - 'embed', - 'fieldset', - 'form', - 'frame', - 'frameset', - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'head', - 'hr', - 'iframe', - 'image', - 'img', - 'input', - 'isindex', - 'li', - 'link', - 'listing', - 'menu', - 'meta', - 'noembed', - 'noframes', - 'noscript', - 'ol', - 'optgroup', - 'option', - 'p', - 'param', - 'plaintext', - 'pre', - 'script', - 'select', - 'spacer', - 'style', - 'tbody', - 'textarea', - 'tfoot', - 'thead', - 'title', - 'tr', - 'ul', - 'wbr' - ); - - // The different phases. - const INIT_PHASE = 0; - const ROOT_PHASE = 1; - const MAIN_PHASE = 2; - const END_PHASE = 3; - - // The different insertion modes for the main phase. - const BEFOR_HEAD = 0; - const IN_HEAD = 1; - const AFTER_HEAD = 2; - const IN_BODY = 3; - const IN_TABLE = 4; - const IN_CAPTION = 5; - const IN_CGROUP = 6; - const IN_TBODY = 7; - const IN_ROW = 8; - const IN_CELL = 9; - const IN_SELECT = 10; - const AFTER_BODY = 11; - const IN_FRAME = 12; - const AFTR_FRAME = 13; - - // The different types of elements. - const SPECIAL = 0; - const SCOPING = 1; - const FORMATTING = 2; - const PHRASING = 3; - - const MARKER = 0; - - public function __construct() - { - $this->phase = self::INIT_PHASE; - $this->mode = self::BEFOR_HEAD; - $this->dom = new DOMDocument; - - $this->dom->encoding = 'UTF-8'; - $this->dom->preserveWhiteSpace = true; - $this->dom->substituteEntities = true; - $this->dom->strictErrorChecking = false; - } - - // Process tag tokens - public function emitToken($token) - { - switch ($this->phase) { - case self::INIT_PHASE: - return $this->initPhase($token); - break; - case self::ROOT_PHASE: - return $this->rootElementPhase($token); - break; - case self::MAIN_PHASE: - return $this->mainPhase($token); - break; - case self::END_PHASE : - return $this->trailingEndPhase($token); - break; - } - } - - private function initPhase($token) - { - /* Initially, the tree construction stage must handle each token - emitted from the tokenisation stage as follows: */ - - /* A DOCTYPE token that is marked as being in error - A comment token - A start tag token - An end tag token - A character token that is not one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE - An end-of-file token */ - if ((isset($token['error']) && $token['error']) || - $token['type'] === HTML5::COMMENT || - $token['type'] === HTML5::STARTTAG || - $token['type'] === HTML5::ENDTAG || - $token['type'] === HTML5::EOF || - ($token['type'] === HTML5::CHARACTR && isset($token['data']) && - !preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) - ) { - /* This specification does not define how to handle this case. In - particular, user agents may ignore the entirety of this specification - altogether for such documents, and instead invoke special parse modes - with a greater emphasis on backwards compatibility. */ - - $this->phase = self::ROOT_PHASE; - return $this->rootElementPhase($token); - - /* A DOCTYPE token marked as being correct */ - } elseif (isset($token['error']) && !$token['error']) { - /* Append a DocumentType node to the Document node, with the name - attribute set to the name given in the DOCTYPE token (which will be - "HTML"), and the other attributes specific to DocumentType objects - set to null, empty lists, or the empty string as appropriate. */ - $doctype = new DOMDocumentType(null, null, 'HTML'); - - /* Then, switch to the root element phase of the tree construction - stage. */ - $this->phase = self::ROOT_PHASE; - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE */ - } elseif (isset($token['data']) && preg_match( - '/^[\t\n\x0b\x0c ]+$/', - $token['data'] - ) - ) { - /* Append that character to the Document node. */ - $text = $this->dom->createTextNode($token['data']); - $this->dom->appendChild($text); - } - } - - private function rootElementPhase($token) - { - /* After the initial phase, as each token is emitted from the tokenisation - stage, it must be processed as described in this section. */ - - /* A DOCTYPE token */ - if ($token['type'] === HTML5::DOCTYPE) { - // Parse error. Ignore the token. - - /* A comment token */ - } elseif ($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the Document object with the data - attribute set to the data given in the comment token. */ - $comment = $this->dom->createComment($token['data']); - $this->dom->appendChild($comment); - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE */ - } elseif ($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) - ) { - /* Append that character to the Document node. */ - $text = $this->dom->createTextNode($token['data']); - $this->dom->appendChild($text); - - /* A character token that is not one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED - (FF), or U+0020 SPACE - A start tag token - An end tag token - An end-of-file token */ - } elseif (($token['type'] === HTML5::CHARACTR && - !preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || - $token['type'] === HTML5::STARTTAG || - $token['type'] === HTML5::ENDTAG || - $token['type'] === HTML5::EOF - ) { - /* Create an HTMLElement node with the tag name html, in the HTML - namespace. Append it to the Document object. Switch to the main - phase and reprocess the current token. */ - $html = $this->dom->createElement('html'); - $this->dom->appendChild($html); - $this->stack[] = $html; - - $this->phase = self::MAIN_PHASE; - return $this->mainPhase($token); - } - } - - private function mainPhase($token) - { - /* Tokens in the main phase must be handled as follows: */ - - /* A DOCTYPE token */ - if ($token['type'] === HTML5::DOCTYPE) { - // Parse error. Ignore the token. - - /* A start tag token with the tag name "html" */ - } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'html') { - /* If this start tag token was not the first start tag token, then - it is a parse error. */ - - /* For each attribute on the token, check to see if the attribute - is already present on the top element of the stack of open elements. - If it is not, add the attribute and its corresponding value to that - element. */ - foreach ($token['attr'] as $attr) { - if (!$this->stack[0]->hasAttribute($attr['name'])) { - $this->stack[0]->setAttribute($attr['name'], $attr['value']); - } - } - - /* An end-of-file token */ - } elseif ($token['type'] === HTML5::EOF) { - /* Generate implied end tags. */ - $this->generateImpliedEndTags(); - - /* Anything else. */ - } else { - /* Depends on the insertion mode: */ - switch ($this->mode) { - case self::BEFOR_HEAD: - return $this->beforeHead($token); - break; - case self::IN_HEAD: - return $this->inHead($token); - break; - case self::AFTER_HEAD: - return $this->afterHead($token); - break; - case self::IN_BODY: - return $this->inBody($token); - break; - case self::IN_TABLE: - return $this->inTable($token); - break; - case self::IN_CAPTION: - return $this->inCaption($token); - break; - case self::IN_CGROUP: - return $this->inColumnGroup($token); - break; - case self::IN_TBODY: - return $this->inTableBody($token); - break; - case self::IN_ROW: - return $this->inRow($token); - break; - case self::IN_CELL: - return $this->inCell($token); - break; - case self::IN_SELECT: - return $this->inSelect($token); - break; - case self::AFTER_BODY: - return $this->afterBody($token); - break; - case self::IN_FRAME: - return $this->inFrameset($token); - break; - case self::AFTR_FRAME: - return $this->afterFrameset($token); - break; - case self::END_PHASE: - return $this->trailingEndPhase($token); - break; - } - } - } - - private function beforeHead($token) - { - /* Handle the token as follows: */ - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE */ - if ($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) - ) { - /* Append the character to the current node. */ - $this->insertText($token['data']); - - /* A comment token */ - } elseif ($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the current node with the data attribute - set to the data given in the comment token. */ - $this->insertComment($token['data']); - - /* A start tag token with the tag name "head" */ - } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'head') { - /* Create an element for the token, append the new element to the - current node and push it onto the stack of open elements. */ - $element = $this->insertElement($token); - - /* Set the head element pointer to this new element node. */ - $this->head_pointer = $element; - - /* Change the insertion mode to "in head". */ - $this->mode = self::IN_HEAD; - - /* A start tag token whose tag name is one of: "base", "link", "meta", - "script", "style", "title". Or an end tag with the tag name "html". - Or a character token that is not one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE. Or any other start tag token */ - } elseif ($token['type'] === HTML5::STARTTAG || - ($token['type'] === HTML5::ENDTAG && $token['name'] === 'html') || - ($token['type'] === HTML5::CHARACTR && !preg_match( - '/^[\t\n\x0b\x0c ]$/', - $token['data'] - )) - ) { - /* Act as if a start tag token with the tag name "head" and no - attributes had been seen, then reprocess the current token. */ - $this->beforeHead( - array( - 'name' => 'head', - 'type' => HTML5::STARTTAG, - 'attr' => array() - ) - ); - - return $this->inHead($token); - - /* Any other end tag */ - } elseif ($token['type'] === HTML5::ENDTAG) { - /* Parse error. Ignore the token. */ - } - } - - private function inHead($token) - { - /* Handle the token as follows: */ - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE. - - THIS DIFFERS FROM THE SPEC: If the current node is either a title, style - or script element, append the character to the current node regardless - of its content. */ - if (($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || ( - $token['type'] === HTML5::CHARACTR && in_array( - end($this->stack)->nodeName, - array('title', 'style', 'script') - )) - ) { - /* Append the character to the current node. */ - $this->insertText($token['data']); - - /* A comment token */ - } elseif ($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the current node with the data attribute - set to the data given in the comment token. */ - $this->insertComment($token['data']); - - } elseif ($token['type'] === HTML5::ENDTAG && - in_array($token['name'], array('title', 'style', 'script')) - ) { - array_pop($this->stack); - return HTML5::PCDATA; - - /* A start tag with the tag name "title" */ - } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'title') { - /* Create an element for the token and append the new element to the - node pointed to by the head element pointer, or, if that is null - (innerHTML case), to the current node. */ - if ($this->head_pointer !== null) { - $element = $this->insertElement($token, false); - $this->head_pointer->appendChild($element); - - } else { - $element = $this->insertElement($token); - } - - /* Switch the tokeniser's content model flag to the RCDATA state. */ - return HTML5::RCDATA; - - /* A start tag with the tag name "style" */ - } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'style') { - /* Create an element for the token and append the new element to the - node pointed to by the head element pointer, or, if that is null - (innerHTML case), to the current node. */ - if ($this->head_pointer !== null) { - $element = $this->insertElement($token, false); - $this->head_pointer->appendChild($element); - - } else { - $this->insertElement($token); - } - - /* Switch the tokeniser's content model flag to the CDATA state. */ - return HTML5::CDATA; - - /* A start tag with the tag name "script" */ - } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'script') { - /* Create an element for the token. */ - $element = $this->insertElement($token, false); - $this->head_pointer->appendChild($element); - - /* Switch the tokeniser's content model flag to the CDATA state. */ - return HTML5::CDATA; - - /* A start tag with the tag name "base", "link", or "meta" */ - } elseif ($token['type'] === HTML5::STARTTAG && in_array( - $token['name'], - array('base', 'link', 'meta') - ) - ) { - /* Create an element for the token and append the new element to the - node pointed to by the head element pointer, or, if that is null - (innerHTML case), to the current node. */ - if ($this->head_pointer !== null) { - $element = $this->insertElement($token, false); - $this->head_pointer->appendChild($element); - array_pop($this->stack); - - } else { - $this->insertElement($token); - } - - /* An end tag with the tag name "head" */ - } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'head') { - /* If the current node is a head element, pop the current node off - the stack of open elements. */ - if ($this->head_pointer->isSameNode(end($this->stack))) { - array_pop($this->stack); - - /* Otherwise, this is a parse error. */ - } else { - // k - } - - /* Change the insertion mode to "after head". */ - $this->mode = self::AFTER_HEAD; - - /* A start tag with the tag name "head" or an end tag except "html". */ - } elseif (($token['type'] === HTML5::STARTTAG && $token['name'] === 'head') || - ($token['type'] === HTML5::ENDTAG && $token['name'] !== 'html') - ) { - // Parse error. Ignore the token. - - /* Anything else */ - } else { - /* If the current node is a head element, act as if an end tag - token with the tag name "head" had been seen. */ - if ($this->head_pointer->isSameNode(end($this->stack))) { - $this->inHead( - array( - 'name' => 'head', - 'type' => HTML5::ENDTAG - ) - ); - - /* Otherwise, change the insertion mode to "after head". */ - } else { - $this->mode = self::AFTER_HEAD; - } - - /* Then, reprocess the current token. */ - return $this->afterHead($token); - } - } - - private function afterHead($token) - { - /* Handle the token as follows: */ - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE */ - if ($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) - ) { - /* Append the character to the current node. */ - $this->insertText($token['data']); - - /* A comment token */ - } elseif ($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the current node with the data attribute - set to the data given in the comment token. */ - $this->insertComment($token['data']); - - /* A start tag token with the tag name "body" */ - } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'body') { - /* Insert a body element for the token. */ - $this->insertElement($token); - - /* Change the insertion mode to "in body". */ - $this->mode = self::IN_BODY; - - /* A start tag token with the tag name "frameset" */ - } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'frameset') { - /* Insert a frameset element for the token. */ - $this->insertElement($token); - - /* Change the insertion mode to "in frameset". */ - $this->mode = self::IN_FRAME; - - /* A start tag token whose tag name is one of: "base", "link", "meta", - "script", "style", "title" */ - } elseif ($token['type'] === HTML5::STARTTAG && in_array( - $token['name'], - array('base', 'link', 'meta', 'script', 'style', 'title') - ) - ) { - /* Parse error. Switch the insertion mode back to "in head" and - reprocess the token. */ - $this->mode = self::IN_HEAD; - return $this->inHead($token); - - /* Anything else */ - } else { - /* Act as if a start tag token with the tag name "body" and no - attributes had been seen, and then reprocess the current token. */ - $this->afterHead( - array( - 'name' => 'body', - 'type' => HTML5::STARTTAG, - 'attr' => array() - ) - ); - - return $this->inBody($token); - } - } - - private function inBody($token) - { - /* Handle the token as follows: */ - - switch ($token['type']) { - /* A character token */ - case HTML5::CHARACTR: - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Append the token's character to the current node. */ - $this->insertText($token['data']); - break; - - /* A comment token */ - case HTML5::COMMENT: - /* Append a Comment node to the current node with the data - attribute set to the data given in the comment token. */ - $this->insertComment($token['data']); - break; - - case HTML5::STARTTAG: - switch ($token['name']) { - /* A start tag token whose tag name is one of: "script", - "style" */ - case 'script': - case 'style': - /* Process the token as if the insertion mode had been "in - head". */ - return $this->inHead($token); - break; - - /* A start tag token whose tag name is one of: "base", "link", - "meta", "title" */ - case 'base': - case 'link': - case 'meta': - case 'title': - /* Parse error. Process the token as if the insertion mode - had been "in head". */ - return $this->inHead($token); - break; - - /* A start tag token with the tag name "body" */ - case 'body': - /* Parse error. If the second element on the stack of open - elements is not a body element, or, if the stack of open - elements has only one node on it, then ignore the token. - (innerHTML case) */ - if (count($this->stack) === 1 || $this->stack[1]->nodeName !== 'body') { - // Ignore - - /* Otherwise, for each attribute on the token, check to see - if the attribute is already present on the body element (the - second element) on the stack of open elements. If it is not, - add the attribute and its corresponding value to that - element. */ - } else { - foreach ($token['attr'] as $attr) { - if (!$this->stack[1]->hasAttribute($attr['name'])) { - $this->stack[1]->setAttribute($attr['name'], $attr['value']); - } - } - } - break; - - /* A start tag whose tag name is one of: "address", - "blockquote", "center", "dir", "div", "dl", "fieldset", - "listing", "menu", "ol", "p", "ul" */ - case 'address': - case 'blockquote': - case 'center': - case 'dir': - case 'div': - case 'dl': - case 'fieldset': - case 'listing': - case 'menu': - case 'ol': - case 'p': - case 'ul': - /* If the stack of open elements has a p element in scope, - then act as if an end tag with the tag name p had been - seen. */ - if ($this->elementInScope('p')) { - $this->emitToken( - array( - 'name' => 'p', - 'type' => HTML5::ENDTAG - ) - ); - } - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - break; - - /* A start tag whose tag name is "form" */ - case 'form': - /* If the form element pointer is not null, ignore the - token with a parse error. */ - if ($this->form_pointer !== null) { - // Ignore. - - /* Otherwise: */ - } else { - /* If the stack of open elements has a p element in - scope, then act as if an end tag with the tag name p - had been seen. */ - if ($this->elementInScope('p')) { - $this->emitToken( - array( - 'name' => 'p', - 'type' => HTML5::ENDTAG - ) - ); - } - - /* Insert an HTML element for the token, and set the - form element pointer to point to the element created. */ - $element = $this->insertElement($token); - $this->form_pointer = $element; - } - break; - - /* A start tag whose tag name is "li", "dd" or "dt" */ - case 'li': - case 'dd': - case 'dt': - /* If the stack of open elements has a p element in scope, - then act as if an end tag with the tag name p had been - seen. */ - if ($this->elementInScope('p')) { - $this->emitToken( - array( - 'name' => 'p', - 'type' => HTML5::ENDTAG - ) - ); - } - - $stack_length = count($this->stack) - 1; - - for ($n = $stack_length; 0 <= $n; $n--) { - /* 1. Initialise node to be the current node (the - bottommost node of the stack). */ - $stop = false; - $node = $this->stack[$n]; - $cat = $this->getElementCategory($node->tagName); - - /* 2. If node is an li, dd or dt element, then pop all - the nodes from the current node up to node, including - node, then stop this algorithm. */ - if ($token['name'] === $node->tagName || ($token['name'] !== 'li' - && ($node->tagName === 'dd' || $node->tagName === 'dt')) - ) { - for ($x = $stack_length; $x >= $n; $x--) { - array_pop($this->stack); - } - - break; - } - - /* 3. If node is not in the formatting category, and is - not in the phrasing category, and is not an address or - div element, then stop this algorithm. */ - if ($cat !== self::FORMATTING && $cat !== self::PHRASING && - $node->tagName !== 'address' && $node->tagName !== 'div' - ) { - break; - } - } - - /* Finally, insert an HTML element with the same tag - name as the token's. */ - $this->insertElement($token); - break; - - /* A start tag token whose tag name is "plaintext" */ - case 'plaintext': - /* If the stack of open elements has a p element in scope, - then act as if an end tag with the tag name p had been - seen. */ - if ($this->elementInScope('p')) { - $this->emitToken( - array( - 'name' => 'p', - 'type' => HTML5::ENDTAG - ) - ); - } - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - return HTML5::PLAINTEXT; - break; - - /* A start tag whose tag name is one of: "h1", "h2", "h3", "h4", - "h5", "h6" */ - case 'h1': - case 'h2': - case 'h3': - case 'h4': - case 'h5': - case 'h6': - /* If the stack of open elements has a p element in scope, - then act as if an end tag with the tag name p had been seen. */ - if ($this->elementInScope('p')) { - $this->emitToken( - array( - 'name' => 'p', - 'type' => HTML5::ENDTAG - ) - ); - } - - /* If the stack of open elements has in scope an element whose - tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then - this is a parse error; pop elements from the stack until an - element with one of those tag names has been popped from the - stack. */ - while ($this->elementInScope(array('h1', 'h2', 'h3', 'h4', 'h5', 'h6'))) { - array_pop($this->stack); - } - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - break; - - /* A start tag whose tag name is "a" */ - case 'a': - /* If the list of active formatting elements contains - an element whose tag name is "a" between the end of the - list and the last marker on the list (or the start of - the list if there is no marker on the list), then this - is a parse error; act as if an end tag with the tag name - "a" had been seen, then remove that element from the list - of active formatting elements and the stack of open - elements if the end tag didn't already remove it (it - might not have if the element is not in table scope). */ - $leng = count($this->a_formatting); - - for ($n = $leng - 1; $n >= 0; $n--) { - if ($this->a_formatting[$n] === self::MARKER) { - break; - - } elseif ($this->a_formatting[$n]->nodeName === 'a') { - $this->emitToken( - array( - 'name' => 'a', - 'type' => HTML5::ENDTAG - ) - ); - break; - } - } - - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Insert an HTML element for the token. */ - $el = $this->insertElement($token); - - /* Add that element to the list of active formatting - elements. */ - $this->a_formatting[] = $el; - break; - - /* A start tag whose tag name is one of: "b", "big", "em", "font", - "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */ - case 'b': - case 'big': - case 'em': - case 'font': - case 'i': - case 'nobr': - case 's': - case 'small': - case 'strike': - case 'strong': - case 'tt': - case 'u': - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Insert an HTML element for the token. */ - $el = $this->insertElement($token); - - /* Add that element to the list of active formatting - elements. */ - $this->a_formatting[] = $el; - break; - - /* A start tag token whose tag name is "button" */ - case 'button': - /* If the stack of open elements has a button element in scope, - then this is a parse error; act as if an end tag with the tag - name "button" had been seen, then reprocess the token. (We don't - do that. Unnecessary.) */ - if ($this->elementInScope('button')) { - $this->inBody( - array( - 'name' => 'button', - 'type' => HTML5::ENDTAG - ) - ); - } - - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* Insert a marker at the end of the list of active - formatting elements. */ - $this->a_formatting[] = self::MARKER; - break; - - /* A start tag token whose tag name is one of: "marquee", "object" */ - case 'marquee': - case 'object': - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* Insert a marker at the end of the list of active - formatting elements. */ - $this->a_formatting[] = self::MARKER; - break; - - /* A start tag token whose tag name is "xmp" */ - case 'xmp': - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* Switch the content model flag to the CDATA state. */ - return HTML5::CDATA; - break; - - /* A start tag whose tag name is "table" */ - case 'table': - /* If the stack of open elements has a p element in scope, - then act as if an end tag with the tag name p had been seen. */ - if ($this->elementInScope('p')) { - $this->emitToken( - array( - 'name' => 'p', - 'type' => HTML5::ENDTAG - ) - ); - } - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* Change the insertion mode to "in table". */ - $this->mode = self::IN_TABLE; - break; - - /* A start tag whose tag name is one of: "area", "basefont", - "bgsound", "br", "embed", "img", "param", "spacer", "wbr" */ - case 'area': - case 'basefont': - case 'bgsound': - case 'br': - case 'embed': - case 'img': - case 'param': - case 'spacer': - case 'wbr': - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* Immediately pop the current node off the stack of open elements. */ - array_pop($this->stack); - break; - - /* A start tag whose tag name is "hr" */ - case 'hr': - /* If the stack of open elements has a p element in scope, - then act as if an end tag with the tag name p had been seen. */ - if ($this->elementInScope('p')) { - $this->emitToken( - array( - 'name' => 'p', - 'type' => HTML5::ENDTAG - ) - ); - } - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* Immediately pop the current node off the stack of open elements. */ - array_pop($this->stack); - break; - - /* A start tag whose tag name is "image" */ - case 'image': - /* Parse error. Change the token's tag name to "img" and - reprocess it. (Don't ask.) */ - $token['name'] = 'img'; - return $this->inBody($token); - break; - - /* A start tag whose tag name is "input" */ - case 'input': - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Insert an input element for the token. */ - $element = $this->insertElement($token, false); - - /* If the form element pointer is not null, then associate the - input element with the form element pointed to by the form - element pointer. */ - $this->form_pointer !== null - ? $this->form_pointer->appendChild($element) - : end($this->stack)->appendChild($element); - - /* Pop that input element off the stack of open elements. */ - array_pop($this->stack); - break; - - /* A start tag whose tag name is "isindex" */ - case 'isindex': - /* Parse error. */ - // w/e - - /* If the form element pointer is not null, - then ignore the token. */ - if ($this->form_pointer === null) { - /* Act as if a start tag token with the tag name "form" had - been seen. */ - $this->inBody( - array( - 'name' => 'body', - 'type' => HTML5::STARTTAG, - 'attr' => array() - ) - ); - - /* Act as if a start tag token with the tag name "hr" had - been seen. */ - $this->inBody( - array( - 'name' => 'hr', - 'type' => HTML5::STARTTAG, - 'attr' => array() - ) - ); - - /* Act as if a start tag token with the tag name "p" had - been seen. */ - $this->inBody( - array( - 'name' => 'p', - 'type' => HTML5::STARTTAG, - 'attr' => array() - ) - ); - - /* Act as if a start tag token with the tag name "label" - had been seen. */ - $this->inBody( - array( - 'name' => 'label', - 'type' => HTML5::STARTTAG, - 'attr' => array() - ) - ); - - /* Act as if a stream of character tokens had been seen. */ - $this->insertText( - 'This is a searchable index. ' . - 'Insert your search keywords here: ' - ); - - /* Act as if a start tag token with the tag name "input" - had been seen, with all the attributes from the "isindex" - token, except with the "name" attribute set to the value - "isindex" (ignoring any explicit "name" attribute). */ - $attr = $token['attr']; - $attr[] = array('name' => 'name', 'value' => 'isindex'); - - $this->inBody( - array( - 'name' => 'input', - 'type' => HTML5::STARTTAG, - 'attr' => $attr - ) - ); - - /* Act as if a stream of character tokens had been seen - (see below for what they should say). */ - $this->insertText( - 'This is a searchable index. ' . - 'Insert your search keywords here: ' - ); - - /* Act as if an end tag token with the tag name "label" - had been seen. */ - $this->inBody( - array( - 'name' => 'label', - 'type' => HTML5::ENDTAG - ) - ); - - /* Act as if an end tag token with the tag name "p" had - been seen. */ - $this->inBody( - array( - 'name' => 'p', - 'type' => HTML5::ENDTAG - ) - ); - - /* Act as if a start tag token with the tag name "hr" had - been seen. */ - $this->inBody( - array( - 'name' => 'hr', - 'type' => HTML5::ENDTAG - ) - ); - - /* Act as if an end tag token with the tag name "form" had - been seen. */ - $this->inBody( - array( - 'name' => 'form', - 'type' => HTML5::ENDTAG - ) - ); - } - break; - - /* A start tag whose tag name is "textarea" */ - case 'textarea': - $this->insertElement($token); - - /* Switch the tokeniser's content model flag to the - RCDATA state. */ - return HTML5::RCDATA; - break; - - /* A start tag whose tag name is one of: "iframe", "noembed", - "noframes" */ - case 'iframe': - case 'noembed': - case 'noframes': - $this->insertElement($token); - - /* Switch the tokeniser's content model flag to the CDATA state. */ - return HTML5::CDATA; - break; - - /* A start tag whose tag name is "select" */ - case 'select': - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* Change the insertion mode to "in select". */ - $this->mode = self::IN_SELECT; - break; - - /* A start or end tag whose tag name is one of: "caption", "col", - "colgroup", "frame", "frameset", "head", "option", "optgroup", - "tbody", "td", "tfoot", "th", "thead", "tr". */ - case 'caption': - case 'col': - case 'colgroup': - case 'frame': - case 'frameset': - case 'head': - case 'option': - case 'optgroup': - case 'tbody': - case 'td': - case 'tfoot': - case 'th': - case 'thead': - case 'tr': - // Parse error. Ignore the token. - break; - - /* A start or end tag whose tag name is one of: "event-source", - "section", "nav", "article", "aside", "header", "footer", - "datagrid", "command" */ - case 'event-source': - case 'section': - case 'nav': - case 'article': - case 'aside': - case 'header': - case 'footer': - case 'datagrid': - case 'command': - // Work in progress! - break; - - /* A start tag token not covered by the previous entries */ - default: - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - $this->insertElement($token, true, true); - break; - } - break; - - case HTML5::ENDTAG: - switch ($token['name']) { - /* An end tag with the tag name "body" */ - case 'body': - /* If the second element in the stack of open elements is - not a body element, this is a parse error. Ignore the token. - (innerHTML case) */ - if (count($this->stack) < 2 || $this->stack[1]->nodeName !== 'body') { - // Ignore. - - /* If the current node is not the body element, then this - is a parse error. */ - } elseif (end($this->stack)->nodeName !== 'body') { - // Parse error. - } - - /* Change the insertion mode to "after body". */ - $this->mode = self::AFTER_BODY; - break; - - /* An end tag with the tag name "html" */ - case 'html': - /* Act as if an end tag with tag name "body" had been seen, - then, if that token wasn't ignored, reprocess the current - token. */ - $this->inBody( - array( - 'name' => 'body', - 'type' => HTML5::ENDTAG - ) - ); - - return $this->afterBody($token); - break; - - /* An end tag whose tag name is one of: "address", "blockquote", - "center", "dir", "div", "dl", "fieldset", "listing", "menu", - "ol", "pre", "ul" */ - case 'address': - case 'blockquote': - case 'center': - case 'dir': - case 'div': - case 'dl': - case 'fieldset': - case 'listing': - case 'menu': - case 'ol': - case 'pre': - case 'ul': - /* If the stack of open elements has an element in scope - with the same tag name as that of the token, then generate - implied end tags. */ - if ($this->elementInScope($token['name'])) { - $this->generateImpliedEndTags(); - - /* Now, if the current node is not an element with - the same tag name as that of the token, then this - is a parse error. */ - // w/e - - /* If the stack of open elements has an element in - scope with the same tag name as that of the token, - then pop elements from this stack until an element - with that tag name has been popped from the stack. */ - for ($n = count($this->stack) - 1; $n >= 0; $n--) { - if ($this->stack[$n]->nodeName === $token['name']) { - $n = -1; - } - - array_pop($this->stack); - } - } - break; - - /* An end tag whose tag name is "form" */ - case 'form': - /* If the stack of open elements has an element in scope - with the same tag name as that of the token, then generate - implied end tags. */ - if ($this->elementInScope($token['name'])) { - $this->generateImpliedEndTags(); - - } - - if (end($this->stack)->nodeName !== $token['name']) { - /* Now, if the current node is not an element with the - same tag name as that of the token, then this is a parse - error. */ - // w/e - - } else { - /* Otherwise, if the current node is an element with - the same tag name as that of the token pop that element - from the stack. */ - array_pop($this->stack); - } - - /* In any case, set the form element pointer to null. */ - $this->form_pointer = null; - break; - - /* An end tag whose tag name is "p" */ - case 'p': - /* If the stack of open elements has a p element in scope, - then generate implied end tags, except for p elements. */ - if ($this->elementInScope('p')) { - $this->generateImpliedEndTags(array('p')); - - /* If the current node is not a p element, then this is - a parse error. */ - // k - - /* If the stack of open elements has a p element in - scope, then pop elements from this stack until the stack - no longer has a p element in scope. */ - for ($n = count($this->stack) - 1; $n >= 0; $n--) { - if ($this->elementInScope('p')) { - array_pop($this->stack); - - } else { - break; - } - } - } - break; - - /* An end tag whose tag name is "dd", "dt", or "li" */ - case 'dd': - case 'dt': - case 'li': - /* If the stack of open elements has an element in scope - whose tag name matches the tag name of the token, then - generate implied end tags, except for elements with the - same tag name as the token. */ - if ($this->elementInScope($token['name'])) { - $this->generateImpliedEndTags(array($token['name'])); - - /* If the current node is not an element with the same - tag name as the token, then this is a parse error. */ - // w/e - - /* If the stack of open elements has an element in scope - whose tag name matches the tag name of the token, then - pop elements from this stack until an element with that - tag name has been popped from the stack. */ - for ($n = count($this->stack) - 1; $n >= 0; $n--) { - if ($this->stack[$n]->nodeName === $token['name']) { - $n = -1; - } - - array_pop($this->stack); - } - } - break; - - /* An end tag whose tag name is one of: "h1", "h2", "h3", "h4", - "h5", "h6" */ - case 'h1': - case 'h2': - case 'h3': - case 'h4': - case 'h5': - case 'h6': - $elements = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6'); - - /* If the stack of open elements has in scope an element whose - tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then - generate implied end tags. */ - if ($this->elementInScope($elements)) { - $this->generateImpliedEndTags(); - - /* Now, if the current node is not an element with the same - tag name as that of the token, then this is a parse error. */ - // w/e - - /* If the stack of open elements has in scope an element - whose tag name is one of "h1", "h2", "h3", "h4", "h5", or - "h6", then pop elements from the stack until an element - with one of those tag names has been popped from the stack. */ - while ($this->elementInScope($elements)) { - array_pop($this->stack); - } - } - break; - - /* An end tag whose tag name is one of: "a", "b", "big", "em", - "font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */ - case 'a': - case 'b': - case 'big': - case 'em': - case 'font': - case 'i': - case 'nobr': - case 's': - case 'small': - case 'strike': - case 'strong': - case 'tt': - case 'u': - /* 1. Let the formatting element be the last element in - the list of active formatting elements that: - * is between the end of the list and the last scope - marker in the list, if any, or the start of the list - otherwise, and - * has the same tag name as the token. - */ - while (true) { - for ($a = count($this->a_formatting) - 1; $a >= 0; $a--) { - if ($this->a_formatting[$a] === self::MARKER) { - break; - - } elseif ($this->a_formatting[$a]->tagName === $token['name']) { - $formatting_element = $this->a_formatting[$a]; - $in_stack = in_array($formatting_element, $this->stack, true); - $fe_af_pos = $a; - break; - } - } - - /* If there is no such node, or, if that node is - also in the stack of open elements but the element - is not in scope, then this is a parse error. Abort - these steps. The token is ignored. */ - if (!isset($formatting_element) || ($in_stack && - !$this->elementInScope($token['name'])) - ) { - break; - - /* Otherwise, if there is such a node, but that node - is not in the stack of open elements, then this is a - parse error; remove the element from the list, and - abort these steps. */ - } elseif (isset($formatting_element) && !$in_stack) { - unset($this->a_formatting[$fe_af_pos]); - $this->a_formatting = array_merge($this->a_formatting); - break; - } - - /* 2. Let the furthest block be the topmost node in the - stack of open elements that is lower in the stack - than the formatting element, and is not an element in - the phrasing or formatting categories. There might - not be one. */ - $fe_s_pos = array_search($formatting_element, $this->stack, true); - $length = count($this->stack); - - for ($s = $fe_s_pos + 1; $s < $length; $s++) { - $category = $this->getElementCategory($this->stack[$s]->nodeName); - - if ($category !== self::PHRASING && $category !== self::FORMATTING) { - $furthest_block = $this->stack[$s]; - } - } - - /* 3. If there is no furthest block, then the UA must - skip the subsequent steps and instead just pop all - the nodes from the bottom of the stack of open - elements, from the current node up to the formatting - element, and remove the formatting element from the - list of active formatting elements. */ - if (!isset($furthest_block)) { - for ($n = $length - 1; $n >= $fe_s_pos; $n--) { - array_pop($this->stack); - } - - unset($this->a_formatting[$fe_af_pos]); - $this->a_formatting = array_merge($this->a_formatting); - break; - } - - /* 4. Let the common ancestor be the element - immediately above the formatting element in the stack - of open elements. */ - $common_ancestor = $this->stack[$fe_s_pos - 1]; - - /* 5. If the furthest block has a parent node, then - remove the furthest block from its parent node. */ - if ($furthest_block->parentNode !== null) { - $furthest_block->parentNode->removeChild($furthest_block); - } - - /* 6. Let a bookmark note the position of the - formatting element in the list of active formatting - elements relative to the elements on either side - of it in the list. */ - $bookmark = $fe_af_pos; - - /* 7. Let node and last node be the furthest block. - Follow these steps: */ - $node = $furthest_block; - $last_node = $furthest_block; - - while (true) { - for ($n = array_search($node, $this->stack, true) - 1; $n >= 0; $n--) { - /* 7.1 Let node be the element immediately - prior to node in the stack of open elements. */ - $node = $this->stack[$n]; - - /* 7.2 If node is not in the list of active - formatting elements, then remove node from - the stack of open elements and then go back - to step 1. */ - if (!in_array($node, $this->a_formatting, true)) { - unset($this->stack[$n]); - $this->stack = array_merge($this->stack); - - } else { - break; - } - } - - /* 7.3 Otherwise, if node is the formatting - element, then go to the next step in the overall - algorithm. */ - if ($node === $formatting_element) { - break; - - /* 7.4 Otherwise, if last node is the furthest - block, then move the aforementioned bookmark to - be immediately after the node in the list of - active formatting elements. */ - } elseif ($last_node === $furthest_block) { - $bookmark = array_search($node, $this->a_formatting, true) + 1; - } - - /* 7.5 If node has any children, perform a - shallow clone of node, replace the entry for - node in the list of active formatting elements - with an entry for the clone, replace the entry - for node in the stack of open elements with an - entry for the clone, and let node be the clone. */ - if ($node->hasChildNodes()) { - $clone = $node->cloneNode(); - $s_pos = array_search($node, $this->stack, true); - $a_pos = array_search($node, $this->a_formatting, true); - - $this->stack[$s_pos] = $clone; - $this->a_formatting[$a_pos] = $clone; - $node = $clone; - } - - /* 7.6 Insert last node into node, first removing - it from its previous parent node if any. */ - if ($last_node->parentNode !== null) { - $last_node->parentNode->removeChild($last_node); - } - - $node->appendChild($last_node); - - /* 7.7 Let last node be node. */ - $last_node = $node; - } - - /* 8. Insert whatever last node ended up being in - the previous step into the common ancestor node, - first removing it from its previous parent node if - any. */ - if ($last_node->parentNode !== null) { - $last_node->parentNode->removeChild($last_node); - } - - $common_ancestor->appendChild($last_node); - - /* 9. Perform a shallow clone of the formatting - element. */ - $clone = $formatting_element->cloneNode(); - - /* 10. Take all of the child nodes of the furthest - block and append them to the clone created in the - last step. */ - while ($furthest_block->hasChildNodes()) { - $child = $furthest_block->firstChild; - $furthest_block->removeChild($child); - $clone->appendChild($child); - } - - /* 11. Append that clone to the furthest block. */ - $furthest_block->appendChild($clone); - - /* 12. Remove the formatting element from the list - of active formatting elements, and insert the clone - into the list of active formatting elements at the - position of the aforementioned bookmark. */ - $fe_af_pos = array_search($formatting_element, $this->a_formatting, true); - unset($this->a_formatting[$fe_af_pos]); - $this->a_formatting = array_merge($this->a_formatting); - - $af_part1 = array_slice($this->a_formatting, 0, $bookmark - 1); - $af_part2 = array_slice($this->a_formatting, $bookmark, count($this->a_formatting)); - $this->a_formatting = array_merge($af_part1, array($clone), $af_part2); - - /* 13. Remove the formatting element from the stack - of open elements, and insert the clone into the stack - of open elements immediately after (i.e. in a more - deeply nested position than) the position of the - furthest block in that stack. */ - $fe_s_pos = array_search($formatting_element, $this->stack, true); - $fb_s_pos = array_search($furthest_block, $this->stack, true); - unset($this->stack[$fe_s_pos]); - - $s_part1 = array_slice($this->stack, 0, $fb_s_pos); - $s_part2 = array_slice($this->stack, $fb_s_pos + 1, count($this->stack)); - $this->stack = array_merge($s_part1, array($clone), $s_part2); - - /* 14. Jump back to step 1 in this series of steps. */ - unset($formatting_element, $fe_af_pos, $fe_s_pos, $furthest_block); - } - break; - - /* An end tag token whose tag name is one of: "button", - "marquee", "object" */ - case 'button': - case 'marquee': - case 'object': - /* If the stack of open elements has an element in scope whose - tag name matches the tag name of the token, then generate implied - tags. */ - if ($this->elementInScope($token['name'])) { - $this->generateImpliedEndTags(); - - /* Now, if the current node is not an element with the same - tag name as the token, then this is a parse error. */ - // k - - /* Now, if the stack of open elements has an element in scope - whose tag name matches the tag name of the token, then pop - elements from the stack until that element has been popped from - the stack, and clear the list of active formatting elements up - to the last marker. */ - for ($n = count($this->stack) - 1; $n >= 0; $n--) { - if ($this->stack[$n]->nodeName === $token['name']) { - $n = -1; - } - - array_pop($this->stack); - } - - $marker = end(array_keys($this->a_formatting, self::MARKER, true)); - - for ($n = count($this->a_formatting) - 1; $n > $marker; $n--) { - array_pop($this->a_formatting); - } - } - break; - - /* Or an end tag whose tag name is one of: "area", "basefont", - "bgsound", "br", "embed", "hr", "iframe", "image", "img", - "input", "isindex", "noembed", "noframes", "param", "select", - "spacer", "table", "textarea", "wbr" */ - case 'area': - case 'basefont': - case 'bgsound': - case 'br': - case 'embed': - case 'hr': - case 'iframe': - case 'image': - case 'img': - case 'input': - case 'isindex': - case 'noembed': - case 'noframes': - case 'param': - case 'select': - case 'spacer': - case 'table': - case 'textarea': - case 'wbr': - // Parse error. Ignore the token. - break; - - /* An end tag token not covered by the previous entries */ - default: - for ($n = count($this->stack) - 1; $n >= 0; $n--) { - /* Initialise node to be the current node (the bottommost - node of the stack). */ - $node = end($this->stack); - - /* If node has the same tag name as the end tag token, - then: */ - if ($token['name'] === $node->nodeName) { - /* Generate implied end tags. */ - $this->generateImpliedEndTags(); - - /* If the tag name of the end tag token does not - match the tag name of the current node, this is a - parse error. */ - // k - - /* Pop all the nodes from the current node up to - node, including node, then stop this algorithm. */ - for ($x = count($this->stack) - $n; $x >= $n; $x--) { - array_pop($this->stack); - } - - } else { - $category = $this->getElementCategory($node); - - if ($category !== self::SPECIAL && $category !== self::SCOPING) { - /* Otherwise, if node is in neither the formatting - category nor the phrasing category, then this is a - parse error. Stop this algorithm. The end tag token - is ignored. */ - return false; - } - } - } - break; - } - break; - } - } - - private function inTable($token) - { - $clear = array('html', 'table'); - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE */ - if ($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) - ) { - /* Append the character to the current node. */ - $text = $this->dom->createTextNode($token['data']); - end($this->stack)->appendChild($text); - - /* A comment token */ - } elseif ($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the current node with the data - attribute set to the data given in the comment token. */ - $comment = $this->dom->createComment($token['data']); - end($this->stack)->appendChild($comment); - - /* A start tag whose tag name is "caption" */ - } elseif ($token['type'] === HTML5::STARTTAG && - $token['name'] === 'caption' - ) { - /* Clear the stack back to a table context. */ - $this->clearStackToTableContext($clear); - - /* Insert a marker at the end of the list of active - formatting elements. */ - $this->a_formatting[] = self::MARKER; - - /* Insert an HTML element for the token, then switch the - insertion mode to "in caption". */ - $this->insertElement($token); - $this->mode = self::IN_CAPTION; - - /* A start tag whose tag name is "colgroup" */ - } elseif ($token['type'] === HTML5::STARTTAG && - $token['name'] === 'colgroup' - ) { - /* Clear the stack back to a table context. */ - $this->clearStackToTableContext($clear); - - /* Insert an HTML element for the token, then switch the - insertion mode to "in column group". */ - $this->insertElement($token); - $this->mode = self::IN_CGROUP; - - /* A start tag whose tag name is "col" */ - } elseif ($token['type'] === HTML5::STARTTAG && - $token['name'] === 'col' - ) { - $this->inTable( - array( - 'name' => 'colgroup', - 'type' => HTML5::STARTTAG, - 'attr' => array() - ) - ); - - $this->inColumnGroup($token); - - /* A start tag whose tag name is one of: "tbody", "tfoot", "thead" */ - } elseif ($token['type'] === HTML5::STARTTAG && in_array( - $token['name'], - array('tbody', 'tfoot', 'thead') - ) - ) { - /* Clear the stack back to a table context. */ - $this->clearStackToTableContext($clear); - - /* Insert an HTML element for the token, then switch the insertion - mode to "in table body". */ - $this->insertElement($token); - $this->mode = self::IN_TBODY; - - /* A start tag whose tag name is one of: "td", "th", "tr" */ - } elseif ($token['type'] === HTML5::STARTTAG && - in_array($token['name'], array('td', 'th', 'tr')) - ) { - /* Act as if a start tag token with the tag name "tbody" had been - seen, then reprocess the current token. */ - $this->inTable( - array( - 'name' => 'tbody', - 'type' => HTML5::STARTTAG, - 'attr' => array() - ) - ); - - return $this->inTableBody($token); - - /* A start tag whose tag name is "table" */ - } elseif ($token['type'] === HTML5::STARTTAG && - $token['name'] === 'table' - ) { - /* Parse error. Act as if an end tag token with the tag name "table" - had been seen, then, if that token wasn't ignored, reprocess the - current token. */ - $this->inTable( - array( - 'name' => 'table', - 'type' => HTML5::ENDTAG - ) - ); - - return $this->mainPhase($token); - - /* An end tag whose tag name is "table" */ - } elseif ($token['type'] === HTML5::ENDTAG && - $token['name'] === 'table' - ) { - /* If the stack of open elements does not have an element in table - scope with the same tag name as the token, this is a parse error. - Ignore the token. (innerHTML case) */ - if (!$this->elementInScope($token['name'], true)) { - return false; - - /* Otherwise: */ - } else { - /* Generate implied end tags. */ - $this->generateImpliedEndTags(); - - /* Now, if the current node is not a table element, then this - is a parse error. */ - // w/e - - /* Pop elements from this stack until a table element has been - popped from the stack. */ - while (true) { - $current = end($this->stack)->nodeName; - array_pop($this->stack); - - if ($current === 'table') { - break; - } - } - - /* Reset the insertion mode appropriately. */ - $this->resetInsertionMode(); - } - - /* An end tag whose tag name is one of: "body", "caption", "col", - "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr" */ - } elseif ($token['type'] === HTML5::ENDTAG && in_array( - $token['name'], - array( - 'body', - 'caption', - 'col', - 'colgroup', - 'html', - 'tbody', - 'td', - 'tfoot', - 'th', - 'thead', - 'tr' - ) - ) - ) { - // Parse error. Ignore the token. - - /* Anything else */ - } else { - /* Parse error. Process the token as if the insertion mode was "in - body", with the following exception: */ - - /* If the current node is a table, tbody, tfoot, thead, or tr - element, then, whenever a node would be inserted into the current - node, it must instead be inserted into the foster parent element. */ - if (in_array( - end($this->stack)->nodeName, - array('table', 'tbody', 'tfoot', 'thead', 'tr') - ) - ) { - /* The foster parent element is the parent element of the last - table element in the stack of open elements, if there is a - table element and it has such a parent element. If there is no - table element in the stack of open elements (innerHTML case), - then the foster parent element is the first element in the - stack of open elements (the html element). Otherwise, if there - is a table element in the stack of open elements, but the last - table element in the stack of open elements has no parent, or - its parent node is not an element, then the foster parent - element is the element before the last table element in the - stack of open elements. */ - for ($n = count($this->stack) - 1; $n >= 0; $n--) { - if ($this->stack[$n]->nodeName === 'table') { - $table = $this->stack[$n]; - break; - } - } - - if (isset($table) && $table->parentNode !== null) { - $this->foster_parent = $table->parentNode; - - } elseif (!isset($table)) { - $this->foster_parent = $this->stack[0]; - - } elseif (isset($table) && ($table->parentNode === null || - $table->parentNode->nodeType !== XML_ELEMENT_NODE) - ) { - $this->foster_parent = $this->stack[$n - 1]; - } - } - - $this->inBody($token); - } - } - - private function inCaption($token) - { - /* An end tag whose tag name is "caption" */ - if ($token['type'] === HTML5::ENDTAG && $token['name'] === 'caption') { - /* If the stack of open elements does not have an element in table - scope with the same tag name as the token, this is a parse error. - Ignore the token. (innerHTML case) */ - if (!$this->elementInScope($token['name'], true)) { - // Ignore - - /* Otherwise: */ - } else { - /* Generate implied end tags. */ - $this->generateImpliedEndTags(); - - /* Now, if the current node is not a caption element, then this - is a parse error. */ - // w/e - - /* Pop elements from this stack until a caption element has - been popped from the stack. */ - while (true) { - $node = end($this->stack)->nodeName; - array_pop($this->stack); - - if ($node === 'caption') { - break; - } - } - - /* Clear the list of active formatting elements up to the last - marker. */ - $this->clearTheActiveFormattingElementsUpToTheLastMarker(); - - /* Switch the insertion mode to "in table". */ - $this->mode = self::IN_TABLE; - } - - /* A start tag whose tag name is one of: "caption", "col", "colgroup", - "tbody", "td", "tfoot", "th", "thead", "tr", or an end tag whose tag - name is "table" */ - } elseif (($token['type'] === HTML5::STARTTAG && in_array( - $token['name'], - array( - 'caption', - 'col', - 'colgroup', - 'tbody', - 'td', - 'tfoot', - 'th', - 'thead', - 'tr' - ) - )) || ($token['type'] === HTML5::ENDTAG && - $token['name'] === 'table') - ) { - /* Parse error. Act as if an end tag with the tag name "caption" - had been seen, then, if that token wasn't ignored, reprocess the - current token. */ - $this->inCaption( - array( - 'name' => 'caption', - 'type' => HTML5::ENDTAG - ) - ); - - return $this->inTable($token); - - /* An end tag whose tag name is one of: "body", "col", "colgroup", - "html", "tbody", "td", "tfoot", "th", "thead", "tr" */ - } elseif ($token['type'] === HTML5::ENDTAG && in_array( - $token['name'], - array( - 'body', - 'col', - 'colgroup', - 'html', - 'tbody', - 'tfoot', - 'th', - 'thead', - 'tr' - ) - ) - ) { - // Parse error. Ignore the token. - - /* Anything else */ - } else { - /* Process the token as if the insertion mode was "in body". */ - $this->inBody($token); - } - } - - private function inColumnGroup($token) - { - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE */ - if ($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) - ) { - /* Append the character to the current node. */ - $text = $this->dom->createTextNode($token['data']); - end($this->stack)->appendChild($text); - - /* A comment token */ - } elseif ($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the current node with the data - attribute set to the data given in the comment token. */ - $comment = $this->dom->createComment($token['data']); - end($this->stack)->appendChild($comment); - - /* A start tag whose tag name is "col" */ - } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'col') { - /* Insert a col element for the token. Immediately pop the current - node off the stack of open elements. */ - $this->insertElement($token); - array_pop($this->stack); - - /* An end tag whose tag name is "colgroup" */ - } elseif ($token['type'] === HTML5::ENDTAG && - $token['name'] === 'colgroup' - ) { - /* If the current node is the root html element, then this is a - parse error, ignore the token. (innerHTML case) */ - if (end($this->stack)->nodeName === 'html') { - // Ignore - - /* Otherwise, pop the current node (which will be a colgroup - element) from the stack of open elements. Switch the insertion - mode to "in table". */ - } else { - array_pop($this->stack); - $this->mode = self::IN_TABLE; - } - - /* An end tag whose tag name is "col" */ - } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'col') { - /* Parse error. Ignore the token. */ - - /* Anything else */ - } else { - /* Act as if an end tag with the tag name "colgroup" had been seen, - and then, if that token wasn't ignored, reprocess the current token. */ - $this->inColumnGroup( - array( - 'name' => 'colgroup', - 'type' => HTML5::ENDTAG - ) - ); - - return $this->inTable($token); - } - } - - private function inTableBody($token) - { - $clear = array('tbody', 'tfoot', 'thead', 'html'); - - /* A start tag whose tag name is "tr" */ - if ($token['type'] === HTML5::STARTTAG && $token['name'] === 'tr') { - /* Clear the stack back to a table body context. */ - $this->clearStackToTableContext($clear); - - /* Insert a tr element for the token, then switch the insertion - mode to "in row". */ - $this->insertElement($token); - $this->mode = self::IN_ROW; - - /* A start tag whose tag name is one of: "th", "td" */ - } elseif ($token['type'] === HTML5::STARTTAG && - ($token['name'] === 'th' || $token['name'] === 'td') - ) { - /* Parse error. Act as if a start tag with the tag name "tr" had - been seen, then reprocess the current token. */ - $this->inTableBody( - array( - 'name' => 'tr', - 'type' => HTML5::STARTTAG, - 'attr' => array() - ) - ); - - return $this->inRow($token); - - /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */ - } elseif ($token['type'] === HTML5::ENDTAG && - in_array($token['name'], array('tbody', 'tfoot', 'thead')) - ) { - /* If the stack of open elements does not have an element in table - scope with the same tag name as the token, this is a parse error. - Ignore the token. */ - if (!$this->elementInScope($token['name'], true)) { - // Ignore - - /* Otherwise: */ - } else { - /* Clear the stack back to a table body context. */ - $this->clearStackToTableContext($clear); - - /* Pop the current node from the stack of open elements. Switch - the insertion mode to "in table". */ - array_pop($this->stack); - $this->mode = self::IN_TABLE; - } - - /* A start tag whose tag name is one of: "caption", "col", "colgroup", - "tbody", "tfoot", "thead", or an end tag whose tag name is "table" */ - } elseif (($token['type'] === HTML5::STARTTAG && in_array( - $token['name'], - array('caption', 'col', 'colgroup', 'tbody', 'tfoor', 'thead') - )) || - ($token['type'] === HTML5::STARTTAG && $token['name'] === 'table') - ) { - /* If the stack of open elements does not have a tbody, thead, or - tfoot element in table scope, this is a parse error. Ignore the - token. (innerHTML case) */ - if (!$this->elementInScope(array('tbody', 'thead', 'tfoot'), true)) { - // Ignore. - - /* Otherwise: */ - } else { - /* Clear the stack back to a table body context. */ - $this->clearStackToTableContext($clear); - - /* Act as if an end tag with the same tag name as the current - node ("tbody", "tfoot", or "thead") had been seen, then - reprocess the current token. */ - $this->inTableBody( - array( - 'name' => end($this->stack)->nodeName, - 'type' => HTML5::ENDTAG - ) - ); - - return $this->mainPhase($token); - } - - /* An end tag whose tag name is one of: "body", "caption", "col", - "colgroup", "html", "td", "th", "tr" */ - } elseif ($token['type'] === HTML5::ENDTAG && in_array( - $token['name'], - array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr') - ) - ) { - /* Parse error. Ignore the token. */ - - /* Anything else */ - } else { - /* Process the token as if the insertion mode was "in table". */ - $this->inTable($token); - } - } - - private function inRow($token) - { - $clear = array('tr', 'html'); - - /* A start tag whose tag name is one of: "th", "td" */ - if ($token['type'] === HTML5::STARTTAG && - ($token['name'] === 'th' || $token['name'] === 'td') - ) { - /* Clear the stack back to a table row context. */ - $this->clearStackToTableContext($clear); - - /* Insert an HTML element for the token, then switch the insertion - mode to "in cell". */ - $this->insertElement($token); - $this->mode = self::IN_CELL; - - /* Insert a marker at the end of the list of active formatting - elements. */ - $this->a_formatting[] = self::MARKER; - - /* An end tag whose tag name is "tr" */ - } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'tr') { - /* If the stack of open elements does not have an element in table - scope with the same tag name as the token, this is a parse error. - Ignore the token. (innerHTML case) */ - if (!$this->elementInScope($token['name'], true)) { - // Ignore. - - /* Otherwise: */ - } else { - /* Clear the stack back to a table row context. */ - $this->clearStackToTableContext($clear); - - /* Pop the current node (which will be a tr element) from the - stack of open elements. Switch the insertion mode to "in table - body". */ - array_pop($this->stack); - $this->mode = self::IN_TBODY; - } - - /* A start tag whose tag name is one of: "caption", "col", "colgroup", - "tbody", "tfoot", "thead", "tr" or an end tag whose tag name is "table" */ - } elseif ($token['type'] === HTML5::STARTTAG && in_array( - $token['name'], - array('caption', 'col', 'colgroup', 'tbody', 'tfoot', 'thead', 'tr') - ) - ) { - /* Act as if an end tag with the tag name "tr" had been seen, then, - if that token wasn't ignored, reprocess the current token. */ - $this->inRow( - array( - 'name' => 'tr', - 'type' => HTML5::ENDTAG - ) - ); - - return $this->inCell($token); - - /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */ - } elseif ($token['type'] === HTML5::ENDTAG && - in_array($token['name'], array('tbody', 'tfoot', 'thead')) - ) { - /* If the stack of open elements does not have an element in table - scope with the same tag name as the token, this is a parse error. - Ignore the token. */ - if (!$this->elementInScope($token['name'], true)) { - // Ignore. - - /* Otherwise: */ - } else { - /* Otherwise, act as if an end tag with the tag name "tr" had - been seen, then reprocess the current token. */ - $this->inRow( - array( - 'name' => 'tr', - 'type' => HTML5::ENDTAG - ) - ); - - return $this->inCell($token); - } - - /* An end tag whose tag name is one of: "body", "caption", "col", - "colgroup", "html", "td", "th" */ - } elseif ($token['type'] === HTML5::ENDTAG && in_array( - $token['name'], - array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr') - ) - ) { - /* Parse error. Ignore the token. */ - - /* Anything else */ - } else { - /* Process the token as if the insertion mode was "in table". */ - $this->inTable($token); - } - } - - private function inCell($token) - { - /* An end tag whose tag name is one of: "td", "th" */ - if ($token['type'] === HTML5::ENDTAG && - ($token['name'] === 'td' || $token['name'] === 'th') - ) { - /* If the stack of open elements does not have an element in table - scope with the same tag name as that of the token, then this is a - parse error and the token must be ignored. */ - if (!$this->elementInScope($token['name'], true)) { - // Ignore. - - /* Otherwise: */ - } else { - /* Generate implied end tags, except for elements with the same - tag name as the token. */ - $this->generateImpliedEndTags(array($token['name'])); - - /* Now, if the current node is not an element with the same tag - name as the token, then this is a parse error. */ - // k - - /* Pop elements from this stack until an element with the same - tag name as the token has been popped from the stack. */ - while (true) { - $node = end($this->stack)->nodeName; - array_pop($this->stack); - - if ($node === $token['name']) { - break; - } - } - - /* Clear the list of active formatting elements up to the last - marker. */ - $this->clearTheActiveFormattingElementsUpToTheLastMarker(); - - /* Switch the insertion mode to "in row". (The current node - will be a tr element at this point.) */ - $this->mode = self::IN_ROW; - } - - /* A start tag whose tag name is one of: "caption", "col", "colgroup", - "tbody", "td", "tfoot", "th", "thead", "tr" */ - } elseif ($token['type'] === HTML5::STARTTAG && in_array( - $token['name'], - array( - 'caption', - 'col', - 'colgroup', - 'tbody', - 'td', - 'tfoot', - 'th', - 'thead', - 'tr' - ) - ) - ) { - /* If the stack of open elements does not have a td or th element - in table scope, then this is a parse error; ignore the token. - (innerHTML case) */ - if (!$this->elementInScope(array('td', 'th'), true)) { - // Ignore. - - /* Otherwise, close the cell (see below) and reprocess the current - token. */ - } else { - $this->closeCell(); - return $this->inRow($token); - } - - /* A start tag whose tag name is one of: "caption", "col", "colgroup", - "tbody", "td", "tfoot", "th", "thead", "tr" */ - } elseif ($token['type'] === HTML5::STARTTAG && in_array( - $token['name'], - array( - 'caption', - 'col', - 'colgroup', - 'tbody', - 'td', - 'tfoot', - 'th', - 'thead', - 'tr' - ) - ) - ) { - /* If the stack of open elements does not have a td or th element - in table scope, then this is a parse error; ignore the token. - (innerHTML case) */ - if (!$this->elementInScope(array('td', 'th'), true)) { - // Ignore. - - /* Otherwise, close the cell (see below) and reprocess the current - token. */ - } else { - $this->closeCell(); - return $this->inRow($token); - } - - /* An end tag whose tag name is one of: "body", "caption", "col", - "colgroup", "html" */ - } elseif ($token['type'] === HTML5::ENDTAG && in_array( - $token['name'], - array('body', 'caption', 'col', 'colgroup', 'html') - ) - ) { - /* Parse error. Ignore the token. */ - - /* An end tag whose tag name is one of: "table", "tbody", "tfoot", - "thead", "tr" */ - } elseif ($token['type'] === HTML5::ENDTAG && in_array( - $token['name'], - array('table', 'tbody', 'tfoot', 'thead', 'tr') - ) - ) { - /* If the stack of open elements does not have an element in table - scope with the same tag name as that of the token (which can only - happen for "tbody", "tfoot" and "thead", or, in the innerHTML case), - then this is a parse error and the token must be ignored. */ - if (!$this->elementInScope($token['name'], true)) { - // Ignore. - - /* Otherwise, close the cell (see below) and reprocess the current - token. */ - } else { - $this->closeCell(); - return $this->inRow($token); - } - - /* Anything else */ - } else { - /* Process the token as if the insertion mode was "in body". */ - $this->inBody($token); - } - } - - private function inSelect($token) - { - /* Handle the token as follows: */ - - /* A character token */ - if ($token['type'] === HTML5::CHARACTR) { - /* Append the token's character to the current node. */ - $this->insertText($token['data']); - - /* A comment token */ - } elseif ($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the current node with the data - attribute set to the data given in the comment token. */ - $this->insertComment($token['data']); - - /* A start tag token whose tag name is "option" */ - } elseif ($token['type'] === HTML5::STARTTAG && - $token['name'] === 'option' - ) { - /* If the current node is an option element, act as if an end tag - with the tag name "option" had been seen. */ - if (end($this->stack)->nodeName === 'option') { - $this->inSelect( - array( - 'name' => 'option', - 'type' => HTML5::ENDTAG - ) - ); - } - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* A start tag token whose tag name is "optgroup" */ - } elseif ($token['type'] === HTML5::STARTTAG && - $token['name'] === 'optgroup' - ) { - /* If the current node is an option element, act as if an end tag - with the tag name "option" had been seen. */ - if (end($this->stack)->nodeName === 'option') { - $this->inSelect( - array( - 'name' => 'option', - 'type' => HTML5::ENDTAG - ) - ); - } - - /* If the current node is an optgroup element, act as if an end tag - with the tag name "optgroup" had been seen. */ - if (end($this->stack)->nodeName === 'optgroup') { - $this->inSelect( - array( - 'name' => 'optgroup', - 'type' => HTML5::ENDTAG - ) - ); - } - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* An end tag token whose tag name is "optgroup" */ - } elseif ($token['type'] === HTML5::ENDTAG && - $token['name'] === 'optgroup' - ) { - /* First, if the current node is an option element, and the node - immediately before it in the stack of open elements is an optgroup - element, then act as if an end tag with the tag name "option" had - been seen. */ - $elements_in_stack = count($this->stack); - - if ($this->stack[$elements_in_stack - 1]->nodeName === 'option' && - $this->stack[$elements_in_stack - 2]->nodeName === 'optgroup' - ) { - $this->inSelect( - array( - 'name' => 'option', - 'type' => HTML5::ENDTAG - ) - ); - } - - /* If the current node is an optgroup element, then pop that node - from the stack of open elements. Otherwise, this is a parse error, - ignore the token. */ - if ($this->stack[$elements_in_stack - 1] === 'optgroup') { - array_pop($this->stack); - } - - /* An end tag token whose tag name is "option" */ - } elseif ($token['type'] === HTML5::ENDTAG && - $token['name'] === 'option' - ) { - /* If the current node is an option element, then pop that node - from the stack of open elements. Otherwise, this is a parse error, - ignore the token. */ - if (end($this->stack)->nodeName === 'option') { - array_pop($this->stack); - } - - /* An end tag whose tag name is "select" */ - } elseif ($token['type'] === HTML5::ENDTAG && - $token['name'] === 'select' - ) { - /* If the stack of open elements does not have an element in table - scope with the same tag name as the token, this is a parse error. - Ignore the token. (innerHTML case) */ - if (!$this->elementInScope($token['name'], true)) { - // w/e - - /* Otherwise: */ - } else { - /* Pop elements from the stack of open elements until a select - element has been popped from the stack. */ - while (true) { - $current = end($this->stack)->nodeName; - array_pop($this->stack); - - if ($current === 'select') { - break; - } - } - - /* Reset the insertion mode appropriately. */ - $this->resetInsertionMode(); - } - - /* A start tag whose tag name is "select" */ - } elseif ($token['name'] === 'select' && - $token['type'] === HTML5::STARTTAG - ) { - /* Parse error. Act as if the token had been an end tag with the - tag name "select" instead. */ - $this->inSelect( - array( - 'name' => 'select', - 'type' => HTML5::ENDTAG - ) - ); - - /* An end tag whose tag name is one of: "caption", "table", "tbody", - "tfoot", "thead", "tr", "td", "th" */ - } elseif (in_array( - $token['name'], - array( - 'caption', - 'table', - 'tbody', - 'tfoot', - 'thead', - 'tr', - 'td', - 'th' - ) - ) && $token['type'] === HTML5::ENDTAG - ) { - /* Parse error. */ - // w/e - - /* If the stack of open elements has an element in table scope with - the same tag name as that of the token, then act as if an end tag - with the tag name "select" had been seen, and reprocess the token. - Otherwise, ignore the token. */ - if ($this->elementInScope($token['name'], true)) { - $this->inSelect( - array( - 'name' => 'select', - 'type' => HTML5::ENDTAG - ) - ); - - $this->mainPhase($token); - } - - /* Anything else */ - } else { - /* Parse error. Ignore the token. */ - } - } - - private function afterBody($token) - { - /* Handle the token as follows: */ - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE */ - if ($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) - ) { - /* Process the token as it would be processed if the insertion mode - was "in body". */ - $this->inBody($token); - - /* A comment token */ - } elseif ($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the first element in the stack of open - elements (the html element), with the data attribute set to the - data given in the comment token. */ - $comment = $this->dom->createComment($token['data']); - $this->stack[0]->appendChild($comment); - - /* An end tag with the tag name "html" */ - } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'html') { - /* If the parser was originally created in order to handle the - setting of an element's innerHTML attribute, this is a parse error; - ignore the token. (The element will be an html element in this - case.) (innerHTML case) */ - - /* Otherwise, switch to the trailing end phase. */ - $this->phase = self::END_PHASE; - - /* Anything else */ - } else { - /* Parse error. Set the insertion mode to "in body" and reprocess - the token. */ - $this->mode = self::IN_BODY; - return $this->inBody($token); - } - } - - private function inFrameset($token) - { - /* Handle the token as follows: */ - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */ - if ($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) - ) { - /* Append the character to the current node. */ - $this->insertText($token['data']); - - /* A comment token */ - } elseif ($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the current node with the data - attribute set to the data given in the comment token. */ - $this->insertComment($token['data']); - - /* A start tag with the tag name "frameset" */ - } elseif ($token['name'] === 'frameset' && - $token['type'] === HTML5::STARTTAG - ) { - $this->insertElement($token); - - /* An end tag with the tag name "frameset" */ - } elseif ($token['name'] === 'frameset' && - $token['type'] === HTML5::ENDTAG - ) { - /* If the current node is the root html element, then this is a - parse error; ignore the token. (innerHTML case) */ - if (end($this->stack)->nodeName === 'html') { - // Ignore - - } else { - /* Otherwise, pop the current node from the stack of open - elements. */ - array_pop($this->stack); - - /* If the parser was not originally created in order to handle - the setting of an element's innerHTML attribute (innerHTML case), - and the current node is no longer a frameset element, then change - the insertion mode to "after frameset". */ - $this->mode = self::AFTR_FRAME; - } - - /* A start tag with the tag name "frame" */ - } elseif ($token['name'] === 'frame' && - $token['type'] === HTML5::STARTTAG - ) { - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* Immediately pop the current node off the stack of open elements. */ - array_pop($this->stack); - - /* A start tag with the tag name "noframes" */ - } elseif ($token['name'] === 'noframes' && - $token['type'] === HTML5::STARTTAG - ) { - /* Process the token as if the insertion mode had been "in body". */ - $this->inBody($token); - - /* Anything else */ - } else { - /* Parse error. Ignore the token. */ - } - } - - private function afterFrameset($token) - { - /* Handle the token as follows: */ - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */ - if ($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) - ) { - /* Append the character to the current node. */ - $this->insertText($token['data']); - - /* A comment token */ - } elseif ($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the current node with the data - attribute set to the data given in the comment token. */ - $this->insertComment($token['data']); - - /* An end tag with the tag name "html" */ - } elseif ($token['name'] === 'html' && - $token['type'] === HTML5::ENDTAG - ) { - /* Switch to the trailing end phase. */ - $this->phase = self::END_PHASE; - - /* A start tag with the tag name "noframes" */ - } elseif ($token['name'] === 'noframes' && - $token['type'] === HTML5::STARTTAG - ) { - /* Process the token as if the insertion mode had been "in body". */ - $this->inBody($token); - - /* Anything else */ - } else { - /* Parse error. Ignore the token. */ - } - } - - private function trailingEndPhase($token) - { - /* After the main phase, as each token is emitted from the tokenisation - stage, it must be processed as described in this section. */ - - /* A DOCTYPE token */ - if ($token['type'] === HTML5::DOCTYPE) { - // Parse error. Ignore the token. - - /* A comment token */ - } elseif ($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the Document object with the data - attribute set to the data given in the comment token. */ - $comment = $this->dom->createComment($token['data']); - $this->dom->appendChild($comment); - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE */ - } elseif ($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) - ) { - /* Process the token as it would be processed in the main phase. */ - $this->mainPhase($token); - - /* A character token that is not one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE. Or a start tag token. Or an end tag token. */ - } elseif (($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || - $token['type'] === HTML5::STARTTAG || $token['type'] === HTML5::ENDTAG - ) { - /* Parse error. Switch back to the main phase and reprocess the - token. */ - $this->phase = self::MAIN_PHASE; - return $this->mainPhase($token); - - /* An end-of-file token */ - } elseif ($token['type'] === HTML5::EOF) { - /* OMG DONE!! */ - } - } - - private function insertElement($token, $append = true, $check = false) - { - // Proprietary workaround for libxml2's limitations with tag names - if ($check) { - // Slightly modified HTML5 tag-name modification, - // removing anything that's not an ASCII letter, digit, or hyphen - $token['name'] = preg_replace('/[^a-z0-9-]/i', '', $token['name']); - // Remove leading hyphens and numbers - $token['name'] = ltrim($token['name'], '-0..9'); - // In theory, this should ever be needed, but just in case - if ($token['name'] === '') { - $token['name'] = 'span'; - } // arbitrary generic choice - } - - $el = $this->dom->createElement($token['name']); - - foreach ($token['attr'] as $attr) { - if (!$el->hasAttribute($attr['name'])) { - $el->setAttribute($attr['name'], $attr['value']); - } - } - - $this->appendToRealParent($el); - $this->stack[] = $el; - - return $el; - } - - private function insertText($data) - { - $text = $this->dom->createTextNode($data); - $this->appendToRealParent($text); - } - - private function insertComment($data) - { - $comment = $this->dom->createComment($data); - $this->appendToRealParent($comment); - } - - private function appendToRealParent($node) - { - if ($this->foster_parent === null) { - end($this->stack)->appendChild($node); - - } elseif ($this->foster_parent !== null) { - /* If the foster parent element is the parent element of the - last table element in the stack of open elements, then the new - node must be inserted immediately before the last table element - in the stack of open elements in the foster parent element; - otherwise, the new node must be appended to the foster parent - element. */ - for ($n = count($this->stack) - 1; $n >= 0; $n--) { - if ($this->stack[$n]->nodeName === 'table' && - $this->stack[$n]->parentNode !== null - ) { - $table = $this->stack[$n]; - break; - } - } - - if (isset($table) && $this->foster_parent->isSameNode($table->parentNode)) { - $this->foster_parent->insertBefore($node, $table); - } else { - $this->foster_parent->appendChild($node); - } - - $this->foster_parent = null; - } - } - - private function elementInScope($el, $table = false) - { - if (is_array($el)) { - foreach ($el as $element) { - if ($this->elementInScope($element, $table)) { - return true; - } - } - - return false; - } - - $leng = count($this->stack); - - for ($n = 0; $n < $leng; $n++) { - /* 1. Initialise node to be the current node (the bottommost node of - the stack). */ - $node = $this->stack[$leng - 1 - $n]; - - if ($node->tagName === $el) { - /* 2. If node is the target node, terminate in a match state. */ - return true; - - } elseif ($node->tagName === 'table') { - /* 3. Otherwise, if node is a table element, terminate in a failure - state. */ - return false; - - } elseif ($table === true && in_array( - $node->tagName, - array( - 'caption', - 'td', - 'th', - 'button', - 'marquee', - 'object' - ) - ) - ) { - /* 4. Otherwise, if the algorithm is the "has an element in scope" - variant (rather than the "has an element in table scope" variant), - and node is one of the following, terminate in a failure state. */ - return false; - - } elseif ($node === $node->ownerDocument->documentElement) { - /* 5. Otherwise, if node is an html element (root element), terminate - in a failure state. (This can only happen if the node is the topmost - node of the stack of open elements, and prevents the next step from - being invoked if there are no more elements in the stack.) */ - return false; - } - - /* Otherwise, set node to the previous entry in the stack of open - elements and return to step 2. (This will never fail, since the loop - will always terminate in the previous step if the top of the stack - is reached.) */ - } - } - - private function reconstructActiveFormattingElements() - { - /* 1. If there are no entries in the list of active formatting elements, - then there is nothing to reconstruct; stop this algorithm. */ - $formatting_elements = count($this->a_formatting); - - if ($formatting_elements === 0) { - return false; - } - - /* 3. Let entry be the last (most recently added) element in the list - of active formatting elements. */ - $entry = end($this->a_formatting); - - /* 2. If the last (most recently added) entry in the list of active - formatting elements is a marker, or if it is an element that is in the - stack of open elements, then there is nothing to reconstruct; stop this - algorithm. */ - if ($entry === self::MARKER || in_array($entry, $this->stack, true)) { - return false; - } - - for ($a = $formatting_elements - 1; $a >= 0; true) { - /* 4. If there are no entries before entry in the list of active - formatting elements, then jump to step 8. */ - if ($a === 0) { - $step_seven = false; - break; - } - - /* 5. Let entry be the entry one earlier than entry in the list of - active formatting elements. */ - $a--; - $entry = $this->a_formatting[$a]; - - /* 6. If entry is neither a marker nor an element that is also in - thetack of open elements, go to step 4. */ - if ($entry === self::MARKER || in_array($entry, $this->stack, true)) { - break; - } - } - - while (true) { - /* 7. Let entry be the element one later than entry in the list of - active formatting elements. */ - if (isset($step_seven) && $step_seven === true) { - $a++; - $entry = $this->a_formatting[$a]; - } - - /* 8. Perform a shallow clone of the element entry to obtain clone. */ - $clone = $entry->cloneNode(); - - /* 9. Append clone to the current node and push it onto the stack - of open elements so that it is the new current node. */ - end($this->stack)->appendChild($clone); - $this->stack[] = $clone; - - /* 10. Replace the entry for entry in the list with an entry for - clone. */ - $this->a_formatting[$a] = $clone; - - /* 11. If the entry for clone in the list of active formatting - elements is not the last entry in the list, return to step 7. */ - if (end($this->a_formatting) !== $clone) { - $step_seven = true; - } else { - break; - } - } - } - - private function clearTheActiveFormattingElementsUpToTheLastMarker() - { - /* When the steps below require the UA to clear the list of active - formatting elements up to the last marker, the UA must perform the - following steps: */ - - while (true) { - /* 1. Let entry be the last (most recently added) entry in the list - of active formatting elements. */ - $entry = end($this->a_formatting); - - /* 2. Remove entry from the list of active formatting elements. */ - array_pop($this->a_formatting); - - /* 3. If entry was a marker, then stop the algorithm at this point. - The list has been cleared up to the last marker. */ - if ($entry === self::MARKER) { - break; - } - } - } - - private function generateImpliedEndTags($exclude = array()) - { - /* When the steps below require the UA to generate implied end tags, - then, if the current node is a dd element, a dt element, an li element, - a p element, a td element, a th element, or a tr element, the UA must - act as if an end tag with the respective tag name had been seen and - then generate implied end tags again. */ - $node = end($this->stack); - $elements = array_diff(array('dd', 'dt', 'li', 'p', 'td', 'th', 'tr'), $exclude); - - while (in_array(end($this->stack)->nodeName, $elements)) { - array_pop($this->stack); - } - } - - private function getElementCategory($node) - { - $name = $node->tagName; - if (in_array($name, $this->special)) { - return self::SPECIAL; - } elseif (in_array($name, $this->scoping)) { - return self::SCOPING; - } elseif (in_array($name, $this->formatting)) { - return self::FORMATTING; - } else { - return self::PHRASING; - } - } - - private function clearStackToTableContext($elements) - { - /* When the steps above require the UA to clear the stack back to a - table context, it means that the UA must, while the current node is not - a table element or an html element, pop elements from the stack of open - elements. If this causes any elements to be popped from the stack, then - this is a parse error. */ - while (true) { - $node = end($this->stack)->nodeName; - - if (in_array($node, $elements)) { - break; - } else { - array_pop($this->stack); - } - } - } - - private function resetInsertionMode() - { - /* 1. Let last be false. */ - $last = false; - $leng = count($this->stack); - - for ($n = $leng - 1; $n >= 0; $n--) { - /* 2. Let node be the last node in the stack of open elements. */ - $node = $this->stack[$n]; - - /* 3. If node is the first node in the stack of open elements, then - set last to true. If the element whose innerHTML attribute is being - set is neither a td element nor a th element, then set node to the - element whose innerHTML attribute is being set. (innerHTML case) */ - if ($this->stack[0]->isSameNode($node)) { - $last = true; - } - - /* 4. If node is a select element, then switch the insertion mode to - "in select" and abort these steps. (innerHTML case) */ - if ($node->nodeName === 'select') { - $this->mode = self::IN_SELECT; - break; - - /* 5. If node is a td or th element, then switch the insertion mode - to "in cell" and abort these steps. */ - } elseif ($node->nodeName === 'td' || $node->nodeName === 'th') { - $this->mode = self::IN_CELL; - break; - - /* 6. If node is a tr element, then switch the insertion mode to - "in row" and abort these steps. */ - } elseif ($node->nodeName === 'tr') { - $this->mode = self::IN_ROW; - break; - - /* 7. If node is a tbody, thead, or tfoot element, then switch the - insertion mode to "in table body" and abort these steps. */ - } elseif (in_array($node->nodeName, array('tbody', 'thead', 'tfoot'))) { - $this->mode = self::IN_TBODY; - break; - - /* 8. If node is a caption element, then switch the insertion mode - to "in caption" and abort these steps. */ - } elseif ($node->nodeName === 'caption') { - $this->mode = self::IN_CAPTION; - break; - - /* 9. If node is a colgroup element, then switch the insertion mode - to "in column group" and abort these steps. (innerHTML case) */ - } elseif ($node->nodeName === 'colgroup') { - $this->mode = self::IN_CGROUP; - break; - - /* 10. If node is a table element, then switch the insertion mode - to "in table" and abort these steps. */ - } elseif ($node->nodeName === 'table') { - $this->mode = self::IN_TABLE; - break; - - /* 11. If node is a head element, then switch the insertion mode - to "in body" ("in body"! not "in head"!) and abort these steps. - (innerHTML case) */ - } elseif ($node->nodeName === 'head') { - $this->mode = self::IN_BODY; - break; - - /* 12. If node is a body element, then switch the insertion mode to - "in body" and abort these steps. */ - } elseif ($node->nodeName === 'body') { - $this->mode = self::IN_BODY; - break; - - /* 13. If node is a frameset element, then switch the insertion - mode to "in frameset" and abort these steps. (innerHTML case) */ - } elseif ($node->nodeName === 'frameset') { - $this->mode = self::IN_FRAME; - break; - - /* 14. If node is an html element, then: if the head element - pointer is null, switch the insertion mode to "before head", - otherwise, switch the insertion mode to "after head". In either - case, abort these steps. (innerHTML case) */ - } elseif ($node->nodeName === 'html') { - $this->mode = ($this->head_pointer === null) - ? self::BEFOR_HEAD - : self::AFTER_HEAD; - - break; - - /* 15. If last is true, then set the insertion mode to "in body" - and abort these steps. (innerHTML case) */ - } elseif ($last) { - $this->mode = self::IN_BODY; - break; - } - } - } - - private function closeCell() - { - /* If the stack of open elements has a td or th element in table scope, - then act as if an end tag token with that tag name had been seen. */ - foreach (array('td', 'th') as $cell) { - if ($this->elementInScope($cell, true)) { - $this->inCell( - array( - 'name' => $cell, - 'type' => HTML5::ENDTAG - ) - ); - - break; - } - } - } - - public function save() - { - return $this->dom; - } -} +normalize($html, $config, $context); + $new_html = $this->wrapHTML($new_html, $config, $context, false /* no div */); + try { + $parser = new HTML5($new_html); + $doc = $parser->save(); + } catch (DOMException $e) { + // Uh oh, it failed. Punt to DirectLex. + $lexer = new HTMLPurifier_Lexer_DirectLex(); + $context->register('PH5PError', $e); // save the error, so we can detect it + return $lexer->tokenizeHTML($html, $config, $context); // use original HTML + } + $tokens = array(); + $this->tokenizeDOM( + $doc->getElementsByTagName('html')->item(0)-> // + getElementsByTagName('body')->item(0) // + , + $tokens, $config + ); + return $tokens; + } +} + +/* + +Copyright 2007 Jeroen van der Meer + +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. + +*/ + +class HTML5 +{ + private $data; + private $char; + private $EOF; + private $state; + private $tree; + private $token; + private $content_model; + private $escape = false; + private $entities = array( + 'AElig;', + 'AElig', + 'AMP;', + 'AMP', + 'Aacute;', + 'Aacute', + 'Acirc;', + 'Acirc', + 'Agrave;', + 'Agrave', + 'Alpha;', + 'Aring;', + 'Aring', + 'Atilde;', + 'Atilde', + 'Auml;', + 'Auml', + 'Beta;', + 'COPY;', + 'COPY', + 'Ccedil;', + 'Ccedil', + 'Chi;', + 'Dagger;', + 'Delta;', + 'ETH;', + 'ETH', + 'Eacute;', + 'Eacute', + 'Ecirc;', + 'Ecirc', + 'Egrave;', + 'Egrave', + 'Epsilon;', + 'Eta;', + 'Euml;', + 'Euml', + 'GT;', + 'GT', + 'Gamma;', + 'Iacute;', + 'Iacute', + 'Icirc;', + 'Icirc', + 'Igrave;', + 'Igrave', + 'Iota;', + 'Iuml;', + 'Iuml', + 'Kappa;', + 'LT;', + 'LT', + 'Lambda;', + 'Mu;', + 'Ntilde;', + 'Ntilde', + 'Nu;', + 'OElig;', + 'Oacute;', + 'Oacute', + 'Ocirc;', + 'Ocirc', + 'Ograve;', + 'Ograve', + 'Omega;', + 'Omicron;', + 'Oslash;', + 'Oslash', + 'Otilde;', + 'Otilde', + 'Ouml;', + 'Ouml', + 'Phi;', + 'Pi;', + 'Prime;', + 'Psi;', + 'QUOT;', + 'QUOT', + 'REG;', + 'REG', + 'Rho;', + 'Scaron;', + 'Sigma;', + 'THORN;', + 'THORN', + 'TRADE;', + 'Tau;', + 'Theta;', + 'Uacute;', + 'Uacute', + 'Ucirc;', + 'Ucirc', + 'Ugrave;', + 'Ugrave', + 'Upsilon;', + 'Uuml;', + 'Uuml', + 'Xi;', + 'Yacute;', + 'Yacute', + 'Yuml;', + 'Zeta;', + 'aacute;', + 'aacute', + 'acirc;', + 'acirc', + 'acute;', + 'acute', + 'aelig;', + 'aelig', + 'agrave;', + 'agrave', + 'alefsym;', + 'alpha;', + 'amp;', + 'amp', + 'and;', + 'ang;', + 'apos;', + 'aring;', + 'aring', + 'asymp;', + 'atilde;', + 'atilde', + 'auml;', + 'auml', + 'bdquo;', + 'beta;', + 'brvbar;', + 'brvbar', + 'bull;', + 'cap;', + 'ccedil;', + 'ccedil', + 'cedil;', + 'cedil', + 'cent;', + 'cent', + 'chi;', + 'circ;', + 'clubs;', + 'cong;', + 'copy;', + 'copy', + 'crarr;', + 'cup;', + 'curren;', + 'curren', + 'dArr;', + 'dagger;', + 'darr;', + 'deg;', + 'deg', + 'delta;', + 'diams;', + 'divide;', + 'divide', + 'eacute;', + 'eacute', + 'ecirc;', + 'ecirc', + 'egrave;', + 'egrave', + 'empty;', + 'emsp;', + 'ensp;', + 'epsilon;', + 'equiv;', + 'eta;', + 'eth;', + 'eth', + 'euml;', + 'euml', + 'euro;', + 'exist;', + 'fnof;', + 'forall;', + 'frac12;', + 'frac12', + 'frac14;', + 'frac14', + 'frac34;', + 'frac34', + 'frasl;', + 'gamma;', + 'ge;', + 'gt;', + 'gt', + 'hArr;', + 'harr;', + 'hearts;', + 'hellip;', + 'iacute;', + 'iacute', + 'icirc;', + 'icirc', + 'iexcl;', + 'iexcl', + 'igrave;', + 'igrave', + 'image;', + 'infin;', + 'int;', + 'iota;', + 'iquest;', + 'iquest', + 'isin;', + 'iuml;', + 'iuml', + 'kappa;', + 'lArr;', + 'lambda;', + 'lang;', + 'laquo;', + 'laquo', + 'larr;', + 'lceil;', + 'ldquo;', + 'le;', + 'lfloor;', + 'lowast;', + 'loz;', + 'lrm;', + 'lsaquo;', + 'lsquo;', + 'lt;', + 'lt', + 'macr;', + 'macr', + 'mdash;', + 'micro;', + 'micro', + 'middot;', + 'middot', + 'minus;', + 'mu;', + 'nabla;', + 'nbsp;', + 'nbsp', + 'ndash;', + 'ne;', + 'ni;', + 'not;', + 'not', + 'notin;', + 'nsub;', + 'ntilde;', + 'ntilde', + 'nu;', + 'oacute;', + 'oacute', + 'ocirc;', + 'ocirc', + 'oelig;', + 'ograve;', + 'ograve', + 'oline;', + 'omega;', + 'omicron;', + 'oplus;', + 'or;', + 'ordf;', + 'ordf', + 'ordm;', + 'ordm', + 'oslash;', + 'oslash', + 'otilde;', + 'otilde', + 'otimes;', + 'ouml;', + 'ouml', + 'para;', + 'para', + 'part;', + 'permil;', + 'perp;', + 'phi;', + 'pi;', + 'piv;', + 'plusmn;', + 'plusmn', + 'pound;', + 'pound', + 'prime;', + 'prod;', + 'prop;', + 'psi;', + 'quot;', + 'quot', + 'rArr;', + 'radic;', + 'rang;', + 'raquo;', + 'raquo', + 'rarr;', + 'rceil;', + 'rdquo;', + 'real;', + 'reg;', + 'reg', + 'rfloor;', + 'rho;', + 'rlm;', + 'rsaquo;', + 'rsquo;', + 'sbquo;', + 'scaron;', + 'sdot;', + 'sect;', + 'sect', + 'shy;', + 'shy', + 'sigma;', + 'sigmaf;', + 'sim;', + 'spades;', + 'sub;', + 'sube;', + 'sum;', + 'sup1;', + 'sup1', + 'sup2;', + 'sup2', + 'sup3;', + 'sup3', + 'sup;', + 'supe;', + 'szlig;', + 'szlig', + 'tau;', + 'there4;', + 'theta;', + 'thetasym;', + 'thinsp;', + 'thorn;', + 'thorn', + 'tilde;', + 'times;', + 'times', + 'trade;', + 'uArr;', + 'uacute;', + 'uacute', + 'uarr;', + 'ucirc;', + 'ucirc', + 'ugrave;', + 'ugrave', + 'uml;', + 'uml', + 'upsih;', + 'upsilon;', + 'uuml;', + 'uuml', + 'weierp;', + 'xi;', + 'yacute;', + 'yacute', + 'yen;', + 'yen', + 'yuml;', + 'yuml', + 'zeta;', + 'zwj;', + 'zwnj;' + ); + + const PCDATA = 0; + const RCDATA = 1; + const CDATA = 2; + const PLAINTEXT = 3; + + const DOCTYPE = 0; + const STARTTAG = 1; + const ENDTAG = 2; + const COMMENT = 3; + const CHARACTR = 4; + const EOF = 5; + + public function __construct($data) + { + $this->data = $data; + $this->char = -1; + $this->EOF = strlen($data); + $this->tree = new HTML5TreeConstructer; + $this->content_model = self::PCDATA; + + $this->state = 'data'; + + while ($this->state !== null) { + $this->{$this->state . 'State'}(); + } + } + + public function save() + { + return $this->tree->save(); + } + + private function char() + { + return ($this->char < $this->EOF) + ? $this->data[$this->char] + : false; + } + + private function character($s, $l = 0) + { + if ($s + $l < $this->EOF) { + if ($l === 0) { + return $this->data[$s]; + } else { + return substr($this->data, $s, $l); + } + } + } + + private function characters($char_class, $start) + { + return preg_replace('#^([' . $char_class . ']+).*#s', '\\1', substr($this->data, $start)); + } + + private function dataState() + { + // Consume the next input character + $this->char++; + $char = $this->char(); + + if ($char === '&' && ($this->content_model === self::PCDATA || $this->content_model === self::RCDATA)) { + /* U+0026 AMPERSAND (&) + When the content model flag is set to one of the PCDATA or RCDATA + states: switch to the entity data state. Otherwise: treat it as per + the "anything else" entry below. */ + $this->state = 'entityData'; + + } elseif ($char === '-') { + /* If the content model flag is set to either the RCDATA state or + the CDATA state, and the escape flag is false, and there are at + least three characters before this one in the input stream, and the + last four characters in the input stream, including this one, are + U+003C LESS-THAN SIGN, U+0021 EXCLAMATION MARK, U+002D HYPHEN-MINUS, + and U+002D HYPHEN-MINUS (""), + set the escape flag to false. */ + if (($this->content_model === self::RCDATA || + $this->content_model === self::CDATA) && $this->escape === true && + $this->character($this->char, 3) === '-->' + ) { + $this->escape = false; + } + + /* In any case, emit the input character as a character token. + Stay in the data state. */ + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => $char + ) + ); + + } elseif ($this->char === $this->EOF) { + /* EOF + Emit an end-of-file token. */ + $this->EOF(); + + } elseif ($this->content_model === self::PLAINTEXT) { + /* When the content model flag is set to the PLAINTEXT state + THIS DIFFERS GREATLY FROM THE SPEC: Get the remaining characters of + the text and emit it as a character token. */ + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => substr($this->data, $this->char) + ) + ); + + $this->EOF(); + + } else { + /* Anything else + THIS DIFFERS GREATLY FROM THE SPEC: Get as many character that + otherwise would also be treated as a character token and emit it + as a single character token. Stay in the data state. */ + $len = strcspn($this->data, '<&', $this->char); + $char = substr($this->data, $this->char, $len); + $this->char += $len - 1; + + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => $char + ) + ); + + $this->state = 'data'; + } + } + + private function entityDataState() + { + // Attempt to consume an entity. + $entity = $this->entity(); + + // If nothing is returned, emit a U+0026 AMPERSAND character token. + // Otherwise, emit the character token that was returned. + $char = (!$entity) ? '&' : $entity; + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => $char + ) + ); + + // Finally, switch to the data state. + $this->state = 'data'; + } + + private function tagOpenState() + { + switch ($this->content_model) { + case self::RCDATA: + case self::CDATA: + /* If the next input character is a U+002F SOLIDUS (/) character, + consume it and switch to the close tag open state. If the next + input character is not a U+002F SOLIDUS (/) character, emit a + U+003C LESS-THAN SIGN character token and switch to the data + state to process the next input character. */ + if ($this->character($this->char + 1) === '/') { + $this->char++; + $this->state = 'closeTagOpen'; + + } else { + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => '<' + ) + ); + + $this->state = 'data'; + } + break; + + case self::PCDATA: + // If the content model flag is set to the PCDATA state + // Consume the next input character: + $this->char++; + $char = $this->char(); + + if ($char === '!') { + /* U+0021 EXCLAMATION MARK (!) + Switch to the markup declaration open state. */ + $this->state = 'markupDeclarationOpen'; + + } elseif ($char === '/') { + /* U+002F SOLIDUS (/) + Switch to the close tag open state. */ + $this->state = 'closeTagOpen'; + + } elseif (preg_match('/^[A-Za-z]$/', $char)) { + /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z + Create a new start tag token, set its tag name to the lowercase + version of the input character (add 0x0020 to the character's code + point), then switch to the tag name state. (Don't emit the token + yet; further details will be filled in before it is emitted.) */ + $this->token = array( + 'name' => strtolower($char), + 'type' => self::STARTTAG, + 'attr' => array() + ); + + $this->state = 'tagName'; + + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Parse error. Emit a U+003C LESS-THAN SIGN character token and a + U+003E GREATER-THAN SIGN character token. Switch to the data state. */ + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => '<>' + ) + ); + + $this->state = 'data'; + + } elseif ($char === '?') { + /* U+003F QUESTION MARK (?) + Parse error. Switch to the bogus comment state. */ + $this->state = 'bogusComment'; + + } else { + /* Anything else + Parse error. Emit a U+003C LESS-THAN SIGN character token and + reconsume the current input character in the data state. */ + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => '<' + ) + ); + + $this->char--; + $this->state = 'data'; + } + break; + } + } + + private function closeTagOpenState() + { + $next_node = strtolower($this->characters('A-Za-z', $this->char + 1)); + $the_same = count($this->tree->stack) > 0 && $next_node === end($this->tree->stack)->nodeName; + + if (($this->content_model === self::RCDATA || $this->content_model === self::CDATA) && + (!$the_same || ($the_same && (!preg_match( + '/[\t\n\x0b\x0c >\/]/', + $this->character($this->char + 1 + strlen($next_node)) + ) || $this->EOF === $this->char))) + ) { + /* If the content model flag is set to the RCDATA or CDATA states then + examine the next few characters. If they do not match the tag name of + the last start tag token emitted (case insensitively), or if they do but + they are not immediately followed by one of the following characters: + * U+0009 CHARACTER TABULATION + * U+000A LINE FEED (LF) + * U+000B LINE TABULATION + * U+000C FORM FEED (FF) + * U+0020 SPACE + * U+003E GREATER-THAN SIGN (>) + * U+002F SOLIDUS (/) + * EOF + ...then there is a parse error. Emit a U+003C LESS-THAN SIGN character + token, a U+002F SOLIDUS character token, and switch to the data state + to process the next input character. */ + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => 'state = 'data'; + + } else { + /* Otherwise, if the content model flag is set to the PCDATA state, + or if the next few characters do match that tag name, consume the + next input character: */ + $this->char++; + $char = $this->char(); + + if (preg_match('/^[A-Za-z]$/', $char)) { + /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z + Create a new end tag token, set its tag name to the lowercase version + of the input character (add 0x0020 to the character's code point), then + switch to the tag name state. (Don't emit the token yet; further details + will be filled in before it is emitted.) */ + $this->token = array( + 'name' => strtolower($char), + 'type' => self::ENDTAG + ); + + $this->state = 'tagName'; + + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Parse error. Switch to the data state. */ + $this->state = 'data'; + + } elseif ($this->char === $this->EOF) { + /* EOF + Parse error. Emit a U+003C LESS-THAN SIGN character token and a U+002F + SOLIDUS character token. Reconsume the EOF character in the data state. */ + $this->emitToken( + array( + 'type' => self::CHARACTR, + 'data' => 'char--; + $this->state = 'data'; + + } else { + /* Parse error. Switch to the bogus comment state. */ + $this->state = 'bogusComment'; + } + } + } + + private function tagNameState() + { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Switch to the before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif ($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the EOF + character in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } elseif ($char === '/') { + /* U+002F SOLIDUS (/) + Parse error unless this is a permitted slash. Switch to the before + attribute name state. */ + $this->state = 'beforeAttributeName'; + + } else { + /* Anything else + Append the current input character to the current tag token's tag name. + Stay in the tag name state. */ + $this->token['name'] .= strtolower($char); + $this->state = 'tagName'; + } + } + + private function beforeAttributeNameState() + { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Stay in the before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif ($char === '/') { + /* U+002F SOLIDUS (/) + Parse error unless this is a permitted slash. Stay in the before + attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif ($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the EOF + character in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } else { + /* Anything else + Start a new attribute in the current tag token. Set that attribute's + name to the current input character, and its value to the empty string. + Switch to the attribute name state. */ + $this->token['attr'][] = array( + 'name' => strtolower($char), + 'value' => null + ); + + $this->state = 'attributeName'; + } + } + + private function attributeNameState() + { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Stay in the before attribute name state. */ + $this->state = 'afterAttributeName'; + + } elseif ($char === '=') { + /* U+003D EQUALS SIGN (=) + Switch to the before attribute value state. */ + $this->state = 'beforeAttributeValue'; + + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif ($char === '/' && $this->character($this->char + 1) !== '>') { + /* U+002F SOLIDUS (/) + Parse error unless this is a permitted slash. Switch to the before + attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif ($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the EOF + character in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } else { + /* Anything else + Append the current input character to the current attribute's name. + Stay in the attribute name state. */ + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['name'] .= strtolower($char); + + $this->state = 'attributeName'; + } + } + + private function afterAttributeNameState() + { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Stay in the after attribute name state. */ + $this->state = 'afterAttributeName'; + + } elseif ($char === '=') { + /* U+003D EQUALS SIGN (=) + Switch to the before attribute value state. */ + $this->state = 'beforeAttributeValue'; + + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif ($char === '/' && $this->character($this->char + 1) !== '>') { + /* U+002F SOLIDUS (/) + Parse error unless this is a permitted slash. Switch to the + before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif ($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the EOF + character in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } else { + /* Anything else + Start a new attribute in the current tag token. Set that attribute's + name to the current input character, and its value to the empty string. + Switch to the attribute name state. */ + $this->token['attr'][] = array( + 'name' => strtolower($char), + 'value' => null + ); + + $this->state = 'attributeName'; + } + } + + private function beforeAttributeValueState() + { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Stay in the before attribute value state. */ + $this->state = 'beforeAttributeValue'; + + } elseif ($char === '"') { + /* U+0022 QUOTATION MARK (") + Switch to the attribute value (double-quoted) state. */ + $this->state = 'attributeValueDoubleQuoted'; + + } elseif ($char === '&') { + /* U+0026 AMPERSAND (&) + Switch to the attribute value (unquoted) state and reconsume + this input character. */ + $this->char--; + $this->state = 'attributeValueUnquoted'; + + } elseif ($char === '\'') { + /* U+0027 APOSTROPHE (') + Switch to the attribute value (single-quoted) state. */ + $this->state = 'attributeValueSingleQuoted'; + + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } else { + /* Anything else + Append the current input character to the current attribute's value. + Switch to the attribute value (unquoted) state. */ + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char; + + $this->state = 'attributeValueUnquoted'; + } + } + + private function attributeValueDoubleQuotedState() + { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if ($char === '"') { + /* U+0022 QUOTATION MARK (") + Switch to the before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif ($char === '&') { + /* U+0026 AMPERSAND (&) + Switch to the entity in attribute value state. */ + $this->entityInAttributeValueState('double'); + + } elseif ($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the character + in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } else { + /* Anything else + Append the current input character to the current attribute's value. + Stay in the attribute value (double-quoted) state. */ + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char; + + $this->state = 'attributeValueDoubleQuoted'; + } + } + + private function attributeValueSingleQuotedState() + { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if ($char === '\'') { + /* U+0022 QUOTATION MARK (') + Switch to the before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif ($char === '&') { + /* U+0026 AMPERSAND (&) + Switch to the entity in attribute value state. */ + $this->entityInAttributeValueState('single'); + + } elseif ($this->char === $this->EOF) { + /* EOF + Parse error. Emit the current tag token. Reconsume the character + in the data state. */ + $this->emitToken($this->token); + + $this->char--; + $this->state = 'data'; + + } else { + /* Anything else + Append the current input character to the current attribute's value. + Stay in the attribute value (single-quoted) state. */ + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char; + + $this->state = 'attributeValueSingleQuoted'; + } + } + + private function attributeValueUnquotedState() + { + // Consume the next input character: + $this->char++; + $char = $this->character($this->char); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + /* U+0009 CHARACTER TABULATION + U+000A LINE FEED (LF) + U+000B LINE TABULATION + U+000C FORM FEED (FF) + U+0020 SPACE + Switch to the before attribute name state. */ + $this->state = 'beforeAttributeName'; + + } elseif ($char === '&') { + /* U+0026 AMPERSAND (&) + Switch to the entity in attribute value state. */ + $this->entityInAttributeValueState(); + + } elseif ($char === '>') { + /* U+003E GREATER-THAN SIGN (>) + Emit the current tag token. Switch to the data state. */ + $this->emitToken($this->token); + $this->state = 'data'; + + } else { + /* Anything else + Append the current input character to the current attribute's value. + Stay in the attribute value (unquoted) state. */ + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char; + + $this->state = 'attributeValueUnquoted'; + } + } + + private function entityInAttributeValueState() + { + // Attempt to consume an entity. + $entity = $this->entity(); + + // If nothing is returned, append a U+0026 AMPERSAND character to the + // current attribute's value. Otherwise, emit the character token that + // was returned. + $char = (!$entity) + ? '&' + : $entity; + + $last = count($this->token['attr']) - 1; + $this->token['attr'][$last]['value'] .= $char; + } + + private function bogusCommentState() + { + /* Consume every character up to the first U+003E GREATER-THAN SIGN + character (>) or the end of the file (EOF), whichever comes first. Emit + a comment token whose data is the concatenation of all the characters + starting from and including the character that caused the state machine + to switch into the bogus comment state, up to and including the last + consumed character before the U+003E character, if any, or up to the + end of the file otherwise. (If the comment was started by the end of + the file (EOF), the token is empty.) */ + $data = $this->characters('^>', $this->char); + $this->emitToken( + array( + 'data' => $data, + 'type' => self::COMMENT + ) + ); + + $this->char += strlen($data); + + /* Switch to the data state. */ + $this->state = 'data'; + + /* If the end of the file was reached, reconsume the EOF character. */ + if ($this->char === $this->EOF) { + $this->char = $this->EOF - 1; + } + } + + private function markupDeclarationOpenState() + { + /* If the next two characters are both U+002D HYPHEN-MINUS (-) + characters, consume those two characters, create a comment token whose + data is the empty string, and switch to the comment state. */ + if ($this->character($this->char + 1, 2) === '--') { + $this->char += 2; + $this->state = 'comment'; + $this->token = array( + 'data' => null, + 'type' => self::COMMENT + ); + + /* Otherwise if the next seven chacacters are a case-insensitive match + for the word "DOCTYPE", then consume those characters and switch to the + DOCTYPE state. */ + } elseif (strtolower($this->character($this->char + 1, 7)) === 'doctype') { + $this->char += 7; + $this->state = 'doctype'; + + /* Otherwise, is is a parse error. Switch to the bogus comment state. + The next character that is consumed, if any, is the first character + that will be in the comment. */ + } else { + $this->char++; + $this->state = 'bogusComment'; + } + } + + private function commentState() + { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + /* U+002D HYPHEN-MINUS (-) */ + if ($char === '-') { + /* Switch to the comment dash state */ + $this->state = 'commentDash'; + + /* EOF */ + } elseif ($this->char === $this->EOF) { + /* Parse error. Emit the comment token. Reconsume the EOF character + in the data state. */ + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + /* Anything else */ + } else { + /* Append the input character to the comment token's data. Stay in + the comment state. */ + $this->token['data'] .= $char; + } + } + + private function commentDashState() + { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + /* U+002D HYPHEN-MINUS (-) */ + if ($char === '-') { + /* Switch to the comment end state */ + $this->state = 'commentEnd'; + + /* EOF */ + } elseif ($this->char === $this->EOF) { + /* Parse error. Emit the comment token. Reconsume the EOF character + in the data state. */ + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + /* Anything else */ + } else { + /* Append a U+002D HYPHEN-MINUS (-) character and the input + character to the comment token's data. Switch to the comment state. */ + $this->token['data'] .= '-' . $char; + $this->state = 'comment'; + } + } + + private function commentEndState() + { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if ($char === '>') { + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif ($char === '-') { + $this->token['data'] .= '-'; + + } elseif ($this->char === $this->EOF) { + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + } else { + $this->token['data'] .= '--' . $char; + $this->state = 'comment'; + } + } + + private function doctypeState() + { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + $this->state = 'beforeDoctypeName'; + + } else { + $this->char--; + $this->state = 'beforeDoctypeName'; + } + } + + private function beforeDoctypeNameState() + { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + // Stay in the before DOCTYPE name state. + + } elseif (preg_match('/^[a-z]$/', $char)) { + $this->token = array( + 'name' => strtoupper($char), + 'type' => self::DOCTYPE, + 'error' => true + ); + + $this->state = 'doctypeName'; + + } elseif ($char === '>') { + $this->emitToken( + array( + 'name' => null, + 'type' => self::DOCTYPE, + 'error' => true + ) + ); + + $this->state = 'data'; + + } elseif ($this->char === $this->EOF) { + $this->emitToken( + array( + 'name' => null, + 'type' => self::DOCTYPE, + 'error' => true + ) + ); + + $this->char--; + $this->state = 'data'; + + } else { + $this->token = array( + 'name' => $char, + 'type' => self::DOCTYPE, + 'error' => true + ); + + $this->state = 'doctypeName'; + } + } + + private function doctypeNameState() + { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + $this->state = 'AfterDoctypeName'; + + } elseif ($char === '>') { + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif (preg_match('/^[a-z]$/', $char)) { + $this->token['name'] .= strtoupper($char); + + } elseif ($this->char === $this->EOF) { + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + } else { + $this->token['name'] .= $char; + } + + $this->token['error'] = ($this->token['name'] === 'HTML') + ? false + : true; + } + + private function afterDoctypeNameState() + { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if (preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { + // Stay in the DOCTYPE name state. + + } elseif ($char === '>') { + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif ($this->char === $this->EOF) { + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + } else { + $this->token['error'] = true; + $this->state = 'bogusDoctype'; + } + } + + private function bogusDoctypeState() + { + /* Consume the next input character: */ + $this->char++; + $char = $this->char(); + + if ($char === '>') { + $this->emitToken($this->token); + $this->state = 'data'; + + } elseif ($this->char === $this->EOF) { + $this->emitToken($this->token); + $this->char--; + $this->state = 'data'; + + } else { + // Stay in the bogus DOCTYPE state. + } + } + + private function entity() + { + $start = $this->char; + + // This section defines how to consume an entity. This definition is + // used when parsing entities in text and in attributes. + + // The behaviour depends on the identity of the next character (the + // one immediately after the U+0026 AMPERSAND character): + + switch ($this->character($this->char + 1)) { + // U+0023 NUMBER SIGN (#) + case '#': + + // The behaviour further depends on the character after the + // U+0023 NUMBER SIGN: + switch ($this->character($this->char + 1)) { + // U+0078 LATIN SMALL LETTER X + // U+0058 LATIN CAPITAL LETTER X + case 'x': + case 'X': + // Follow the steps below, but using the range of + // characters U+0030 DIGIT ZERO through to U+0039 DIGIT + // NINE, U+0061 LATIN SMALL LETTER A through to U+0066 + // LATIN SMALL LETTER F, and U+0041 LATIN CAPITAL LETTER + // A, through to U+0046 LATIN CAPITAL LETTER F (in other + // words, 0-9, A-F, a-f). + $char = 1; + $char_class = '0-9A-Fa-f'; + break; + + // Anything else + default: + // Follow the steps below, but using the range of + // characters U+0030 DIGIT ZERO through to U+0039 DIGIT + // NINE (i.e. just 0-9). + $char = 0; + $char_class = '0-9'; + break; + } + + // Consume as many characters as match the range of characters + // given above. + $this->char++; + $e_name = $this->characters($char_class, $this->char + $char + 1); + $entity = $this->character($start, $this->char); + $cond = strlen($e_name) > 0; + + // The rest of the parsing happens below. + break; + + // Anything else + default: + // Consume the maximum number of characters possible, with the + // consumed characters case-sensitively matching one of the + // identifiers in the first column of the entities table. + + $e_name = $this->characters('0-9A-Za-z;', $this->char + 1); + $len = strlen($e_name); + + for ($c = 1; $c <= $len; $c++) { + $id = substr($e_name, 0, $c); + $this->char++; + + if (in_array($id, $this->entities)) { + if ($e_name[$c - 1] !== ';') { + if ($c < $len && $e_name[$c] == ';') { + $this->char++; // consume extra semicolon + } + } + $entity = $id; + break; + } + } + + $cond = isset($entity); + // The rest of the parsing happens below. + break; + } + + if (!$cond) { + // If no match can be made, then this is a parse error. No + // characters are consumed, and nothing is returned. + $this->char = $start; + return false; + } + + // Return a character token for the character corresponding to the + // entity name (as given by the second column of the entities table). + return html_entity_decode('&' . rtrim($entity, ';') . ';', ENT_QUOTES, 'UTF-8'); + } + + private function emitToken($token) + { + $emit = $this->tree->emitToken($token); + + if (is_int($emit)) { + $this->content_model = $emit; + + } elseif ($token['type'] === self::ENDTAG) { + $this->content_model = self::PCDATA; + } + } + + private function EOF() + { + $this->state = null; + $this->tree->emitToken( + array( + 'type' => self::EOF + ) + ); + } +} + +class HTML5TreeConstructer +{ + public $stack = array(); + + private $phase; + private $mode; + private $dom; + private $foster_parent = null; + private $a_formatting = array(); + + private $head_pointer = null; + private $form_pointer = null; + + private $scoping = array('button', 'caption', 'html', 'marquee', 'object', 'table', 'td', 'th'); + private $formatting = array( + 'a', + 'b', + 'big', + 'em', + 'font', + 'i', + 'nobr', + 's', + 'small', + 'strike', + 'strong', + 'tt', + 'u' + ); + private $special = array( + 'address', + 'area', + 'base', + 'basefont', + 'bgsound', + 'blockquote', + 'body', + 'br', + 'center', + 'col', + 'colgroup', + 'dd', + 'dir', + 'div', + 'dl', + 'dt', + 'embed', + 'fieldset', + 'form', + 'frame', + 'frameset', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'head', + 'hr', + 'iframe', + 'image', + 'img', + 'input', + 'isindex', + 'li', + 'link', + 'listing', + 'menu', + 'meta', + 'noembed', + 'noframes', + 'noscript', + 'ol', + 'optgroup', + 'option', + 'p', + 'param', + 'plaintext', + 'pre', + 'script', + 'select', + 'spacer', + 'style', + 'tbody', + 'textarea', + 'tfoot', + 'thead', + 'title', + 'tr', + 'ul', + 'wbr' + ); + + // The different phases. + const INIT_PHASE = 0; + const ROOT_PHASE = 1; + const MAIN_PHASE = 2; + const END_PHASE = 3; + + // The different insertion modes for the main phase. + const BEFOR_HEAD = 0; + const IN_HEAD = 1; + const AFTER_HEAD = 2; + const IN_BODY = 3; + const IN_TABLE = 4; + const IN_CAPTION = 5; + const IN_CGROUP = 6; + const IN_TBODY = 7; + const IN_ROW = 8; + const IN_CELL = 9; + const IN_SELECT = 10; + const AFTER_BODY = 11; + const IN_FRAME = 12; + const AFTR_FRAME = 13; + + // The different types of elements. + const SPECIAL = 0; + const SCOPING = 1; + const FORMATTING = 2; + const PHRASING = 3; + + const MARKER = 0; + + public function __construct() + { + $this->phase = self::INIT_PHASE; + $this->mode = self::BEFOR_HEAD; + $this->dom = new DOMDocument; + + $this->dom->encoding = 'UTF-8'; + $this->dom->preserveWhiteSpace = true; + $this->dom->substituteEntities = true; + $this->dom->strictErrorChecking = false; + } + + // Process tag tokens + public function emitToken($token) + { + switch ($this->phase) { + case self::INIT_PHASE: + return $this->initPhase($token); + break; + case self::ROOT_PHASE: + return $this->rootElementPhase($token); + break; + case self::MAIN_PHASE: + return $this->mainPhase($token); + break; + case self::END_PHASE : + return $this->trailingEndPhase($token); + break; + } + } + + private function initPhase($token) + { + /* Initially, the tree construction stage must handle each token + emitted from the tokenisation stage as follows: */ + + /* A DOCTYPE token that is marked as being in error + A comment token + A start tag token + An end tag token + A character token that is not one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE + An end-of-file token */ + if ((isset($token['error']) && $token['error']) || + $token['type'] === HTML5::COMMENT || + $token['type'] === HTML5::STARTTAG || + $token['type'] === HTML5::ENDTAG || + $token['type'] === HTML5::EOF || + ($token['type'] === HTML5::CHARACTR && isset($token['data']) && + !preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) + ) { + /* This specification does not define how to handle this case. In + particular, user agents may ignore the entirety of this specification + altogether for such documents, and instead invoke special parse modes + with a greater emphasis on backwards compatibility. */ + + $this->phase = self::ROOT_PHASE; + return $this->rootElementPhase($token); + + /* A DOCTYPE token marked as being correct */ + } elseif (isset($token['error']) && !$token['error']) { + /* Append a DocumentType node to the Document node, with the name + attribute set to the name given in the DOCTYPE token (which will be + "HTML"), and the other attributes specific to DocumentType objects + set to null, empty lists, or the empty string as appropriate. */ + $doctype = new DOMDocumentType(null, null, 'HTML'); + + /* Then, switch to the root element phase of the tree construction + stage. */ + $this->phase = self::ROOT_PHASE; + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + } elseif (isset($token['data']) && preg_match( + '/^[\t\n\x0b\x0c ]+$/', + $token['data'] + ) + ) { + /* Append that character to the Document node. */ + $text = $this->dom->createTextNode($token['data']); + $this->dom->appendChild($text); + } + } + + private function rootElementPhase($token) + { + /* After the initial phase, as each token is emitted from the tokenisation + stage, it must be processed as described in this section. */ + + /* A DOCTYPE token */ + if ($token['type'] === HTML5::DOCTYPE) { + // Parse error. Ignore the token. + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the Document object with the data + attribute set to the data given in the comment token. */ + $comment = $this->dom->createComment($token['data']); + $this->dom->appendChild($comment); + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + } elseif ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Append that character to the Document node. */ + $text = $this->dom->createTextNode($token['data']); + $this->dom->appendChild($text); + + /* A character token that is not one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED + (FF), or U+0020 SPACE + A start tag token + An end tag token + An end-of-file token */ + } elseif (($token['type'] === HTML5::CHARACTR && + !preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || + $token['type'] === HTML5::STARTTAG || + $token['type'] === HTML5::ENDTAG || + $token['type'] === HTML5::EOF + ) { + /* Create an HTMLElement node with the tag name html, in the HTML + namespace. Append it to the Document object. Switch to the main + phase and reprocess the current token. */ + $html = $this->dom->createElement('html'); + $this->dom->appendChild($html); + $this->stack[] = $html; + + $this->phase = self::MAIN_PHASE; + return $this->mainPhase($token); + } + } + + private function mainPhase($token) + { + /* Tokens in the main phase must be handled as follows: */ + + /* A DOCTYPE token */ + if ($token['type'] === HTML5::DOCTYPE) { + // Parse error. Ignore the token. + + /* A start tag token with the tag name "html" */ + } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'html') { + /* If this start tag token was not the first start tag token, then + it is a parse error. */ + + /* For each attribute on the token, check to see if the attribute + is already present on the top element of the stack of open elements. + If it is not, add the attribute and its corresponding value to that + element. */ + foreach ($token['attr'] as $attr) { + if (!$this->stack[0]->hasAttribute($attr['name'])) { + $this->stack[0]->setAttribute($attr['name'], $attr['value']); + } + } + + /* An end-of-file token */ + } elseif ($token['type'] === HTML5::EOF) { + /* Generate implied end tags. */ + $this->generateImpliedEndTags(); + + /* Anything else. */ + } else { + /* Depends on the insertion mode: */ + switch ($this->mode) { + case self::BEFOR_HEAD: + return $this->beforeHead($token); + break; + case self::IN_HEAD: + return $this->inHead($token); + break; + case self::AFTER_HEAD: + return $this->afterHead($token); + break; + case self::IN_BODY: + return $this->inBody($token); + break; + case self::IN_TABLE: + return $this->inTable($token); + break; + case self::IN_CAPTION: + return $this->inCaption($token); + break; + case self::IN_CGROUP: + return $this->inColumnGroup($token); + break; + case self::IN_TBODY: + return $this->inTableBody($token); + break; + case self::IN_ROW: + return $this->inRow($token); + break; + case self::IN_CELL: + return $this->inCell($token); + break; + case self::IN_SELECT: + return $this->inSelect($token); + break; + case self::AFTER_BODY: + return $this->afterBody($token); + break; + case self::IN_FRAME: + return $this->inFrameset($token); + break; + case self::AFTR_FRAME: + return $this->afterFrameset($token); + break; + case self::END_PHASE: + return $this->trailingEndPhase($token); + break; + } + } + } + + private function beforeHead($token) + { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + if ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Append the character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data attribute + set to the data given in the comment token. */ + $this->insertComment($token['data']); + + /* A start tag token with the tag name "head" */ + } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'head') { + /* Create an element for the token, append the new element to the + current node and push it onto the stack of open elements. */ + $element = $this->insertElement($token); + + /* Set the head element pointer to this new element node. */ + $this->head_pointer = $element; + + /* Change the insertion mode to "in head". */ + $this->mode = self::IN_HEAD; + + /* A start tag token whose tag name is one of: "base", "link", "meta", + "script", "style", "title". Or an end tag with the tag name "html". + Or a character token that is not one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE. Or any other start tag token */ + } elseif ($token['type'] === HTML5::STARTTAG || + ($token['type'] === HTML5::ENDTAG && $token['name'] === 'html') || + ($token['type'] === HTML5::CHARACTR && !preg_match( + '/^[\t\n\x0b\x0c ]$/', + $token['data'] + )) + ) { + /* Act as if a start tag token with the tag name "head" and no + attributes had been seen, then reprocess the current token. */ + $this->beforeHead( + array( + 'name' => 'head', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + return $this->inHead($token); + + /* Any other end tag */ + } elseif ($token['type'] === HTML5::ENDTAG) { + /* Parse error. Ignore the token. */ + } + } + + private function inHead($token) + { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE. + + THIS DIFFERS FROM THE SPEC: If the current node is either a title, style + or script element, append the character to the current node regardless + of its content. */ + if (($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || ( + $token['type'] === HTML5::CHARACTR && in_array( + end($this->stack)->nodeName, + array('title', 'style', 'script') + )) + ) { + /* Append the character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data attribute + set to the data given in the comment token. */ + $this->insertComment($token['data']); + + } elseif ($token['type'] === HTML5::ENDTAG && + in_array($token['name'], array('title', 'style', 'script')) + ) { + array_pop($this->stack); + return HTML5::PCDATA; + + /* A start tag with the tag name "title" */ + } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'title') { + /* Create an element for the token and append the new element to the + node pointed to by the head element pointer, or, if that is null + (innerHTML case), to the current node. */ + if ($this->head_pointer !== null) { + $element = $this->insertElement($token, false); + $this->head_pointer->appendChild($element); + + } else { + $element = $this->insertElement($token); + } + + /* Switch the tokeniser's content model flag to the RCDATA state. */ + return HTML5::RCDATA; + + /* A start tag with the tag name "style" */ + } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'style') { + /* Create an element for the token and append the new element to the + node pointed to by the head element pointer, or, if that is null + (innerHTML case), to the current node. */ + if ($this->head_pointer !== null) { + $element = $this->insertElement($token, false); + $this->head_pointer->appendChild($element); + + } else { + $this->insertElement($token); + } + + /* Switch the tokeniser's content model flag to the CDATA state. */ + return HTML5::CDATA; + + /* A start tag with the tag name "script" */ + } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'script') { + /* Create an element for the token. */ + $element = $this->insertElement($token, false); + $this->head_pointer->appendChild($element); + + /* Switch the tokeniser's content model flag to the CDATA state. */ + return HTML5::CDATA; + + /* A start tag with the tag name "base", "link", or "meta" */ + } elseif ($token['type'] === HTML5::STARTTAG && in_array( + $token['name'], + array('base', 'link', 'meta') + ) + ) { + /* Create an element for the token and append the new element to the + node pointed to by the head element pointer, or, if that is null + (innerHTML case), to the current node. */ + if ($this->head_pointer !== null) { + $element = $this->insertElement($token, false); + $this->head_pointer->appendChild($element); + array_pop($this->stack); + + } else { + $this->insertElement($token); + } + + /* An end tag with the tag name "head" */ + } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'head') { + /* If the current node is a head element, pop the current node off + the stack of open elements. */ + if ($this->head_pointer->isSameNode(end($this->stack))) { + array_pop($this->stack); + + /* Otherwise, this is a parse error. */ + } else { + // k + } + + /* Change the insertion mode to "after head". */ + $this->mode = self::AFTER_HEAD; + + /* A start tag with the tag name "head" or an end tag except "html". */ + } elseif (($token['type'] === HTML5::STARTTAG && $token['name'] === 'head') || + ($token['type'] === HTML5::ENDTAG && $token['name'] !== 'html') + ) { + // Parse error. Ignore the token. + + /* Anything else */ + } else { + /* If the current node is a head element, act as if an end tag + token with the tag name "head" had been seen. */ + if ($this->head_pointer->isSameNode(end($this->stack))) { + $this->inHead( + array( + 'name' => 'head', + 'type' => HTML5::ENDTAG + ) + ); + + /* Otherwise, change the insertion mode to "after head". */ + } else { + $this->mode = self::AFTER_HEAD; + } + + /* Then, reprocess the current token. */ + return $this->afterHead($token); + } + } + + private function afterHead($token) + { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + if ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Append the character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data attribute + set to the data given in the comment token. */ + $this->insertComment($token['data']); + + /* A start tag token with the tag name "body" */ + } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'body') { + /* Insert a body element for the token. */ + $this->insertElement($token); + + /* Change the insertion mode to "in body". */ + $this->mode = self::IN_BODY; + + /* A start tag token with the tag name "frameset" */ + } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'frameset') { + /* Insert a frameset element for the token. */ + $this->insertElement($token); + + /* Change the insertion mode to "in frameset". */ + $this->mode = self::IN_FRAME; + + /* A start tag token whose tag name is one of: "base", "link", "meta", + "script", "style", "title" */ + } elseif ($token['type'] === HTML5::STARTTAG && in_array( + $token['name'], + array('base', 'link', 'meta', 'script', 'style', 'title') + ) + ) { + /* Parse error. Switch the insertion mode back to "in head" and + reprocess the token. */ + $this->mode = self::IN_HEAD; + return $this->inHead($token); + + /* Anything else */ + } else { + /* Act as if a start tag token with the tag name "body" and no + attributes had been seen, and then reprocess the current token. */ + $this->afterHead( + array( + 'name' => 'body', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + return $this->inBody($token); + } + } + + private function inBody($token) + { + /* Handle the token as follows: */ + + switch ($token['type']) { + /* A character token */ + case HTML5::CHARACTR: + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Append the token's character to the current node. */ + $this->insertText($token['data']); + break; + + /* A comment token */ + case HTML5::COMMENT: + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $this->insertComment($token['data']); + break; + + case HTML5::STARTTAG: + switch ($token['name']) { + /* A start tag token whose tag name is one of: "script", + "style" */ + case 'script': + case 'style': + /* Process the token as if the insertion mode had been "in + head". */ + return $this->inHead($token); + break; + + /* A start tag token whose tag name is one of: "base", "link", + "meta", "title" */ + case 'base': + case 'link': + case 'meta': + case 'title': + /* Parse error. Process the token as if the insertion mode + had been "in head". */ + return $this->inHead($token); + break; + + /* A start tag token with the tag name "body" */ + case 'body': + /* Parse error. If the second element on the stack of open + elements is not a body element, or, if the stack of open + elements has only one node on it, then ignore the token. + (innerHTML case) */ + if (count($this->stack) === 1 || $this->stack[1]->nodeName !== 'body') { + // Ignore + + /* Otherwise, for each attribute on the token, check to see + if the attribute is already present on the body element (the + second element) on the stack of open elements. If it is not, + add the attribute and its corresponding value to that + element. */ + } else { + foreach ($token['attr'] as $attr) { + if (!$this->stack[1]->hasAttribute($attr['name'])) { + $this->stack[1]->setAttribute($attr['name'], $attr['value']); + } + } + } + break; + + /* A start tag whose tag name is one of: "address", + "blockquote", "center", "dir", "div", "dl", "fieldset", + "listing", "menu", "ol", "p", "ul" */ + case 'address': + case 'blockquote': + case 'center': + case 'dir': + case 'div': + case 'dl': + case 'fieldset': + case 'listing': + case 'menu': + case 'ol': + case 'p': + case 'ul': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been + seen. */ + if ($this->elementInScope('p')) { + $this->emitToken( + array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + break; + + /* A start tag whose tag name is "form" */ + case 'form': + /* If the form element pointer is not null, ignore the + token with a parse error. */ + if ($this->form_pointer !== null) { + // Ignore. + + /* Otherwise: */ + } else { + /* If the stack of open elements has a p element in + scope, then act as if an end tag with the tag name p + had been seen. */ + if ($this->elementInScope('p')) { + $this->emitToken( + array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* Insert an HTML element for the token, and set the + form element pointer to point to the element created. */ + $element = $this->insertElement($token); + $this->form_pointer = $element; + } + break; + + /* A start tag whose tag name is "li", "dd" or "dt" */ + case 'li': + case 'dd': + case 'dt': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been + seen. */ + if ($this->elementInScope('p')) { + $this->emitToken( + array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + ) + ); + } + + $stack_length = count($this->stack) - 1; + + for ($n = $stack_length; 0 <= $n; $n--) { + /* 1. Initialise node to be the current node (the + bottommost node of the stack). */ + $stop = false; + $node = $this->stack[$n]; + $cat = $this->getElementCategory($node->tagName); + + /* 2. If node is an li, dd or dt element, then pop all + the nodes from the current node up to node, including + node, then stop this algorithm. */ + if ($token['name'] === $node->tagName || ($token['name'] !== 'li' + && ($node->tagName === 'dd' || $node->tagName === 'dt')) + ) { + for ($x = $stack_length; $x >= $n; $x--) { + array_pop($this->stack); + } + + break; + } + + /* 3. If node is not in the formatting category, and is + not in the phrasing category, and is not an address or + div element, then stop this algorithm. */ + if ($cat !== self::FORMATTING && $cat !== self::PHRASING && + $node->tagName !== 'address' && $node->tagName !== 'div' + ) { + break; + } + } + + /* Finally, insert an HTML element with the same tag + name as the token's. */ + $this->insertElement($token); + break; + + /* A start tag token whose tag name is "plaintext" */ + case 'plaintext': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been + seen. */ + if ($this->elementInScope('p')) { + $this->emitToken( + array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + return HTML5::PLAINTEXT; + break; + + /* A start tag whose tag name is one of: "h1", "h2", "h3", "h4", + "h5", "h6" */ + case 'h1': + case 'h2': + case 'h3': + case 'h4': + case 'h5': + case 'h6': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been seen. */ + if ($this->elementInScope('p')) { + $this->emitToken( + array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* If the stack of open elements has in scope an element whose + tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then + this is a parse error; pop elements from the stack until an + element with one of those tag names has been popped from the + stack. */ + while ($this->elementInScope(array('h1', 'h2', 'h3', 'h4', 'h5', 'h6'))) { + array_pop($this->stack); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + break; + + /* A start tag whose tag name is "a" */ + case 'a': + /* If the list of active formatting elements contains + an element whose tag name is "a" between the end of the + list and the last marker on the list (or the start of + the list if there is no marker on the list), then this + is a parse error; act as if an end tag with the tag name + "a" had been seen, then remove that element from the list + of active formatting elements and the stack of open + elements if the end tag didn't already remove it (it + might not have if the element is not in table scope). */ + $leng = count($this->a_formatting); + + for ($n = $leng - 1; $n >= 0; $n--) { + if ($this->a_formatting[$n] === self::MARKER) { + break; + + } elseif ($this->a_formatting[$n]->nodeName === 'a') { + $this->emitToken( + array( + 'name' => 'a', + 'type' => HTML5::ENDTAG + ) + ); + break; + } + } + + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $el = $this->insertElement($token); + + /* Add that element to the list of active formatting + elements. */ + $this->a_formatting[] = $el; + break; + + /* A start tag whose tag name is one of: "b", "big", "em", "font", + "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */ + case 'b': + case 'big': + case 'em': + case 'font': + case 'i': + case 'nobr': + case 's': + case 'small': + case 'strike': + case 'strong': + case 'tt': + case 'u': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $el = $this->insertElement($token); + + /* Add that element to the list of active formatting + elements. */ + $this->a_formatting[] = $el; + break; + + /* A start tag token whose tag name is "button" */ + case 'button': + /* If the stack of open elements has a button element in scope, + then this is a parse error; act as if an end tag with the tag + name "button" had been seen, then reprocess the token. (We don't + do that. Unnecessary.) */ + if ($this->elementInScope('button')) { + $this->inBody( + array( + 'name' => 'button', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Insert a marker at the end of the list of active + formatting elements. */ + $this->a_formatting[] = self::MARKER; + break; + + /* A start tag token whose tag name is one of: "marquee", "object" */ + case 'marquee': + case 'object': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Insert a marker at the end of the list of active + formatting elements. */ + $this->a_formatting[] = self::MARKER; + break; + + /* A start tag token whose tag name is "xmp" */ + case 'xmp': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Switch the content model flag to the CDATA state. */ + return HTML5::CDATA; + break; + + /* A start tag whose tag name is "table" */ + case 'table': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been seen. */ + if ($this->elementInScope('p')) { + $this->emitToken( + array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Change the insertion mode to "in table". */ + $this->mode = self::IN_TABLE; + break; + + /* A start tag whose tag name is one of: "area", "basefont", + "bgsound", "br", "embed", "img", "param", "spacer", "wbr" */ + case 'area': + case 'basefont': + case 'bgsound': + case 'br': + case 'embed': + case 'img': + case 'param': + case 'spacer': + case 'wbr': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Immediately pop the current node off the stack of open elements. */ + array_pop($this->stack); + break; + + /* A start tag whose tag name is "hr" */ + case 'hr': + /* If the stack of open elements has a p element in scope, + then act as if an end tag with the tag name p had been seen. */ + if ($this->elementInScope('p')) { + $this->emitToken( + array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Immediately pop the current node off the stack of open elements. */ + array_pop($this->stack); + break; + + /* A start tag whose tag name is "image" */ + case 'image': + /* Parse error. Change the token's tag name to "img" and + reprocess it. (Don't ask.) */ + $token['name'] = 'img'; + return $this->inBody($token); + break; + + /* A start tag whose tag name is "input" */ + case 'input': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an input element for the token. */ + $element = $this->insertElement($token, false); + + /* If the form element pointer is not null, then associate the + input element with the form element pointed to by the form + element pointer. */ + $this->form_pointer !== null + ? $this->form_pointer->appendChild($element) + : end($this->stack)->appendChild($element); + + /* Pop that input element off the stack of open elements. */ + array_pop($this->stack); + break; + + /* A start tag whose tag name is "isindex" */ + case 'isindex': + /* Parse error. */ + // w/e + + /* If the form element pointer is not null, + then ignore the token. */ + if ($this->form_pointer === null) { + /* Act as if a start tag token with the tag name "form" had + been seen. */ + $this->inBody( + array( + 'name' => 'body', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + /* Act as if a start tag token with the tag name "hr" had + been seen. */ + $this->inBody( + array( + 'name' => 'hr', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + /* Act as if a start tag token with the tag name "p" had + been seen. */ + $this->inBody( + array( + 'name' => 'p', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + /* Act as if a start tag token with the tag name "label" + had been seen. */ + $this->inBody( + array( + 'name' => 'label', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + /* Act as if a stream of character tokens had been seen. */ + $this->insertText( + 'This is a searchable index. ' . + 'Insert your search keywords here: ' + ); + + /* Act as if a start tag token with the tag name "input" + had been seen, with all the attributes from the "isindex" + token, except with the "name" attribute set to the value + "isindex" (ignoring any explicit "name" attribute). */ + $attr = $token['attr']; + $attr[] = array('name' => 'name', 'value' => 'isindex'); + + $this->inBody( + array( + 'name' => 'input', + 'type' => HTML5::STARTTAG, + 'attr' => $attr + ) + ); + + /* Act as if a stream of character tokens had been seen + (see below for what they should say). */ + $this->insertText( + 'This is a searchable index. ' . + 'Insert your search keywords here: ' + ); + + /* Act as if an end tag token with the tag name "label" + had been seen. */ + $this->inBody( + array( + 'name' => 'label', + 'type' => HTML5::ENDTAG + ) + ); + + /* Act as if an end tag token with the tag name "p" had + been seen. */ + $this->inBody( + array( + 'name' => 'p', + 'type' => HTML5::ENDTAG + ) + ); + + /* Act as if a start tag token with the tag name "hr" had + been seen. */ + $this->inBody( + array( + 'name' => 'hr', + 'type' => HTML5::ENDTAG + ) + ); + + /* Act as if an end tag token with the tag name "form" had + been seen. */ + $this->inBody( + array( + 'name' => 'form', + 'type' => HTML5::ENDTAG + ) + ); + } + break; + + /* A start tag whose tag name is "textarea" */ + case 'textarea': + $this->insertElement($token); + + /* Switch the tokeniser's content model flag to the + RCDATA state. */ + return HTML5::RCDATA; + break; + + /* A start tag whose tag name is one of: "iframe", "noembed", + "noframes" */ + case 'iframe': + case 'noembed': + case 'noframes': + $this->insertElement($token); + + /* Switch the tokeniser's content model flag to the CDATA state. */ + return HTML5::CDATA; + break; + + /* A start tag whose tag name is "select" */ + case 'select': + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Change the insertion mode to "in select". */ + $this->mode = self::IN_SELECT; + break; + + /* A start or end tag whose tag name is one of: "caption", "col", + "colgroup", "frame", "frameset", "head", "option", "optgroup", + "tbody", "td", "tfoot", "th", "thead", "tr". */ + case 'caption': + case 'col': + case 'colgroup': + case 'frame': + case 'frameset': + case 'head': + case 'option': + case 'optgroup': + case 'tbody': + case 'td': + case 'tfoot': + case 'th': + case 'thead': + case 'tr': + // Parse error. Ignore the token. + break; + + /* A start or end tag whose tag name is one of: "event-source", + "section", "nav", "article", "aside", "header", "footer", + "datagrid", "command" */ + case 'event-source': + case 'section': + case 'nav': + case 'article': + case 'aside': + case 'header': + case 'footer': + case 'datagrid': + case 'command': + // Work in progress! + break; + + /* A start tag token not covered by the previous entries */ + default: + /* Reconstruct the active formatting elements, if any. */ + $this->reconstructActiveFormattingElements(); + + $this->insertElement($token, true, true); + break; + } + break; + + case HTML5::ENDTAG: + switch ($token['name']) { + /* An end tag with the tag name "body" */ + case 'body': + /* If the second element in the stack of open elements is + not a body element, this is a parse error. Ignore the token. + (innerHTML case) */ + if (count($this->stack) < 2 || $this->stack[1]->nodeName !== 'body') { + // Ignore. + + /* If the current node is not the body element, then this + is a parse error. */ + } elseif (end($this->stack)->nodeName !== 'body') { + // Parse error. + } + + /* Change the insertion mode to "after body". */ + $this->mode = self::AFTER_BODY; + break; + + /* An end tag with the tag name "html" */ + case 'html': + /* Act as if an end tag with tag name "body" had been seen, + then, if that token wasn't ignored, reprocess the current + token. */ + $this->inBody( + array( + 'name' => 'body', + 'type' => HTML5::ENDTAG + ) + ); + + return $this->afterBody($token); + break; + + /* An end tag whose tag name is one of: "address", "blockquote", + "center", "dir", "div", "dl", "fieldset", "listing", "menu", + "ol", "pre", "ul" */ + case 'address': + case 'blockquote': + case 'center': + case 'dir': + case 'div': + case 'dl': + case 'fieldset': + case 'listing': + case 'menu': + case 'ol': + case 'pre': + case 'ul': + /* If the stack of open elements has an element in scope + with the same tag name as that of the token, then generate + implied end tags. */ + if ($this->elementInScope($token['name'])) { + $this->generateImpliedEndTags(); + + /* Now, if the current node is not an element with + the same tag name as that of the token, then this + is a parse error. */ + // w/e + + /* If the stack of open elements has an element in + scope with the same tag name as that of the token, + then pop elements from this stack until an element + with that tag name has been popped from the stack. */ + for ($n = count($this->stack) - 1; $n >= 0; $n--) { + if ($this->stack[$n]->nodeName === $token['name']) { + $n = -1; + } + + array_pop($this->stack); + } + } + break; + + /* An end tag whose tag name is "form" */ + case 'form': + /* If the stack of open elements has an element in scope + with the same tag name as that of the token, then generate + implied end tags. */ + if ($this->elementInScope($token['name'])) { + $this->generateImpliedEndTags(); + + } + + if (end($this->stack)->nodeName !== $token['name']) { + /* Now, if the current node is not an element with the + same tag name as that of the token, then this is a parse + error. */ + // w/e + + } else { + /* Otherwise, if the current node is an element with + the same tag name as that of the token pop that element + from the stack. */ + array_pop($this->stack); + } + + /* In any case, set the form element pointer to null. */ + $this->form_pointer = null; + break; + + /* An end tag whose tag name is "p" */ + case 'p': + /* If the stack of open elements has a p element in scope, + then generate implied end tags, except for p elements. */ + if ($this->elementInScope('p')) { + $this->generateImpliedEndTags(array('p')); + + /* If the current node is not a p element, then this is + a parse error. */ + // k + + /* If the stack of open elements has a p element in + scope, then pop elements from this stack until the stack + no longer has a p element in scope. */ + for ($n = count($this->stack) - 1; $n >= 0; $n--) { + if ($this->elementInScope('p')) { + array_pop($this->stack); + + } else { + break; + } + } + } + break; + + /* An end tag whose tag name is "dd", "dt", or "li" */ + case 'dd': + case 'dt': + case 'li': + /* If the stack of open elements has an element in scope + whose tag name matches the tag name of the token, then + generate implied end tags, except for elements with the + same tag name as the token. */ + if ($this->elementInScope($token['name'])) { + $this->generateImpliedEndTags(array($token['name'])); + + /* If the current node is not an element with the same + tag name as the token, then this is a parse error. */ + // w/e + + /* If the stack of open elements has an element in scope + whose tag name matches the tag name of the token, then + pop elements from this stack until an element with that + tag name has been popped from the stack. */ + for ($n = count($this->stack) - 1; $n >= 0; $n--) { + if ($this->stack[$n]->nodeName === $token['name']) { + $n = -1; + } + + array_pop($this->stack); + } + } + break; + + /* An end tag whose tag name is one of: "h1", "h2", "h3", "h4", + "h5", "h6" */ + case 'h1': + case 'h2': + case 'h3': + case 'h4': + case 'h5': + case 'h6': + $elements = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6'); + + /* If the stack of open elements has in scope an element whose + tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then + generate implied end tags. */ + if ($this->elementInScope($elements)) { + $this->generateImpliedEndTags(); + + /* Now, if the current node is not an element with the same + tag name as that of the token, then this is a parse error. */ + // w/e + + /* If the stack of open elements has in scope an element + whose tag name is one of "h1", "h2", "h3", "h4", "h5", or + "h6", then pop elements from the stack until an element + with one of those tag names has been popped from the stack. */ + while ($this->elementInScope($elements)) { + array_pop($this->stack); + } + } + break; + + /* An end tag whose tag name is one of: "a", "b", "big", "em", + "font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */ + case 'a': + case 'b': + case 'big': + case 'em': + case 'font': + case 'i': + case 'nobr': + case 's': + case 'small': + case 'strike': + case 'strong': + case 'tt': + case 'u': + /* 1. Let the formatting element be the last element in + the list of active formatting elements that: + * is between the end of the list and the last scope + marker in the list, if any, or the start of the list + otherwise, and + * has the same tag name as the token. + */ + while (true) { + for ($a = count($this->a_formatting) - 1; $a >= 0; $a--) { + if ($this->a_formatting[$a] === self::MARKER) { + break; + + } elseif ($this->a_formatting[$a]->tagName === $token['name']) { + $formatting_element = $this->a_formatting[$a]; + $in_stack = in_array($formatting_element, $this->stack, true); + $fe_af_pos = $a; + break; + } + } + + /* If there is no such node, or, if that node is + also in the stack of open elements but the element + is not in scope, then this is a parse error. Abort + these steps. The token is ignored. */ + if (!isset($formatting_element) || ($in_stack && + !$this->elementInScope($token['name'])) + ) { + break; + + /* Otherwise, if there is such a node, but that node + is not in the stack of open elements, then this is a + parse error; remove the element from the list, and + abort these steps. */ + } elseif (isset($formatting_element) && !$in_stack) { + unset($this->a_formatting[$fe_af_pos]); + $this->a_formatting = array_merge($this->a_formatting); + break; + } + + /* 2. Let the furthest block be the topmost node in the + stack of open elements that is lower in the stack + than the formatting element, and is not an element in + the phrasing or formatting categories. There might + not be one. */ + $fe_s_pos = array_search($formatting_element, $this->stack, true); + $length = count($this->stack); + + for ($s = $fe_s_pos + 1; $s < $length; $s++) { + $category = $this->getElementCategory($this->stack[$s]->nodeName); + + if ($category !== self::PHRASING && $category !== self::FORMATTING) { + $furthest_block = $this->stack[$s]; + } + } + + /* 3. If there is no furthest block, then the UA must + skip the subsequent steps and instead just pop all + the nodes from the bottom of the stack of open + elements, from the current node up to the formatting + element, and remove the formatting element from the + list of active formatting elements. */ + if (!isset($furthest_block)) { + for ($n = $length - 1; $n >= $fe_s_pos; $n--) { + array_pop($this->stack); + } + + unset($this->a_formatting[$fe_af_pos]); + $this->a_formatting = array_merge($this->a_formatting); + break; + } + + /* 4. Let the common ancestor be the element + immediately above the formatting element in the stack + of open elements. */ + $common_ancestor = $this->stack[$fe_s_pos - 1]; + + /* 5. If the furthest block has a parent node, then + remove the furthest block from its parent node. */ + if ($furthest_block->parentNode !== null) { + $furthest_block->parentNode->removeChild($furthest_block); + } + + /* 6. Let a bookmark note the position of the + formatting element in the list of active formatting + elements relative to the elements on either side + of it in the list. */ + $bookmark = $fe_af_pos; + + /* 7. Let node and last node be the furthest block. + Follow these steps: */ + $node = $furthest_block; + $last_node = $furthest_block; + + while (true) { + for ($n = array_search($node, $this->stack, true) - 1; $n >= 0; $n--) { + /* 7.1 Let node be the element immediately + prior to node in the stack of open elements. */ + $node = $this->stack[$n]; + + /* 7.2 If node is not in the list of active + formatting elements, then remove node from + the stack of open elements and then go back + to step 1. */ + if (!in_array($node, $this->a_formatting, true)) { + unset($this->stack[$n]); + $this->stack = array_merge($this->stack); + + } else { + break; + } + } + + /* 7.3 Otherwise, if node is the formatting + element, then go to the next step in the overall + algorithm. */ + if ($node === $formatting_element) { + break; + + /* 7.4 Otherwise, if last node is the furthest + block, then move the aforementioned bookmark to + be immediately after the node in the list of + active formatting elements. */ + } elseif ($last_node === $furthest_block) { + $bookmark = array_search($node, $this->a_formatting, true) + 1; + } + + /* 7.5 If node has any children, perform a + shallow clone of node, replace the entry for + node in the list of active formatting elements + with an entry for the clone, replace the entry + for node in the stack of open elements with an + entry for the clone, and let node be the clone. */ + if ($node->hasChildNodes()) { + $clone = $node->cloneNode(); + $s_pos = array_search($node, $this->stack, true); + $a_pos = array_search($node, $this->a_formatting, true); + + $this->stack[$s_pos] = $clone; + $this->a_formatting[$a_pos] = $clone; + $node = $clone; + } + + /* 7.6 Insert last node into node, first removing + it from its previous parent node if any. */ + if ($last_node->parentNode !== null) { + $last_node->parentNode->removeChild($last_node); + } + + $node->appendChild($last_node); + + /* 7.7 Let last node be node. */ + $last_node = $node; + } + + /* 8. Insert whatever last node ended up being in + the previous step into the common ancestor node, + first removing it from its previous parent node if + any. */ + if ($last_node->parentNode !== null) { + $last_node->parentNode->removeChild($last_node); + } + + $common_ancestor->appendChild($last_node); + + /* 9. Perform a shallow clone of the formatting + element. */ + $clone = $formatting_element->cloneNode(); + + /* 10. Take all of the child nodes of the furthest + block and append them to the clone created in the + last step. */ + while ($furthest_block->hasChildNodes()) { + $child = $furthest_block->firstChild; + $furthest_block->removeChild($child); + $clone->appendChild($child); + } + + /* 11. Append that clone to the furthest block. */ + $furthest_block->appendChild($clone); + + /* 12. Remove the formatting element from the list + of active formatting elements, and insert the clone + into the list of active formatting elements at the + position of the aforementioned bookmark. */ + $fe_af_pos = array_search($formatting_element, $this->a_formatting, true); + unset($this->a_formatting[$fe_af_pos]); + $this->a_formatting = array_merge($this->a_formatting); + + $af_part1 = array_slice($this->a_formatting, 0, $bookmark - 1); + $af_part2 = array_slice($this->a_formatting, $bookmark, count($this->a_formatting)); + $this->a_formatting = array_merge($af_part1, array($clone), $af_part2); + + /* 13. Remove the formatting element from the stack + of open elements, and insert the clone into the stack + of open elements immediately after (i.e. in a more + deeply nested position than) the position of the + furthest block in that stack. */ + $fe_s_pos = array_search($formatting_element, $this->stack, true); + $fb_s_pos = array_search($furthest_block, $this->stack, true); + unset($this->stack[$fe_s_pos]); + + $s_part1 = array_slice($this->stack, 0, $fb_s_pos); + $s_part2 = array_slice($this->stack, $fb_s_pos + 1, count($this->stack)); + $this->stack = array_merge($s_part1, array($clone), $s_part2); + + /* 14. Jump back to step 1 in this series of steps. */ + unset($formatting_element, $fe_af_pos, $fe_s_pos, $furthest_block); + } + break; + + /* An end tag token whose tag name is one of: "button", + "marquee", "object" */ + case 'button': + case 'marquee': + case 'object': + /* If the stack of open elements has an element in scope whose + tag name matches the tag name of the token, then generate implied + tags. */ + if ($this->elementInScope($token['name'])) { + $this->generateImpliedEndTags(); + + /* Now, if the current node is not an element with the same + tag name as the token, then this is a parse error. */ + // k + + /* Now, if the stack of open elements has an element in scope + whose tag name matches the tag name of the token, then pop + elements from the stack until that element has been popped from + the stack, and clear the list of active formatting elements up + to the last marker. */ + for ($n = count($this->stack) - 1; $n >= 0; $n--) { + if ($this->stack[$n]->nodeName === $token['name']) { + $n = -1; + } + + array_pop($this->stack); + } + + $marker = end(array_keys($this->a_formatting, self::MARKER, true)); + + for ($n = count($this->a_formatting) - 1; $n > $marker; $n--) { + array_pop($this->a_formatting); + } + } + break; + + /* Or an end tag whose tag name is one of: "area", "basefont", + "bgsound", "br", "embed", "hr", "iframe", "image", "img", + "input", "isindex", "noembed", "noframes", "param", "select", + "spacer", "table", "textarea", "wbr" */ + case 'area': + case 'basefont': + case 'bgsound': + case 'br': + case 'embed': + case 'hr': + case 'iframe': + case 'image': + case 'img': + case 'input': + case 'isindex': + case 'noembed': + case 'noframes': + case 'param': + case 'select': + case 'spacer': + case 'table': + case 'textarea': + case 'wbr': + // Parse error. Ignore the token. + break; + + /* An end tag token not covered by the previous entries */ + default: + for ($n = count($this->stack) - 1; $n >= 0; $n--) { + /* Initialise node to be the current node (the bottommost + node of the stack). */ + $node = end($this->stack); + + /* If node has the same tag name as the end tag token, + then: */ + if ($token['name'] === $node->nodeName) { + /* Generate implied end tags. */ + $this->generateImpliedEndTags(); + + /* If the tag name of the end tag token does not + match the tag name of the current node, this is a + parse error. */ + // k + + /* Pop all the nodes from the current node up to + node, including node, then stop this algorithm. */ + for ($x = count($this->stack) - $n; $x >= $n; $x--) { + array_pop($this->stack); + } + + } else { + $category = $this->getElementCategory($node); + + if ($category !== self::SPECIAL && $category !== self::SCOPING) { + /* Otherwise, if node is in neither the formatting + category nor the phrasing category, then this is a + parse error. Stop this algorithm. The end tag token + is ignored. */ + return false; + } + } + } + break; + } + break; + } + } + + private function inTable($token) + { + $clear = array('html', 'table'); + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + if ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Append the character to the current node. */ + $text = $this->dom->createTextNode($token['data']); + end($this->stack)->appendChild($text); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $comment = $this->dom->createComment($token['data']); + end($this->stack)->appendChild($comment); + + /* A start tag whose tag name is "caption" */ + } elseif ($token['type'] === HTML5::STARTTAG && + $token['name'] === 'caption' + ) { + /* Clear the stack back to a table context. */ + $this->clearStackToTableContext($clear); + + /* Insert a marker at the end of the list of active + formatting elements. */ + $this->a_formatting[] = self::MARKER; + + /* Insert an HTML element for the token, then switch the + insertion mode to "in caption". */ + $this->insertElement($token); + $this->mode = self::IN_CAPTION; + + /* A start tag whose tag name is "colgroup" */ + } elseif ($token['type'] === HTML5::STARTTAG && + $token['name'] === 'colgroup' + ) { + /* Clear the stack back to a table context. */ + $this->clearStackToTableContext($clear); + + /* Insert an HTML element for the token, then switch the + insertion mode to "in column group". */ + $this->insertElement($token); + $this->mode = self::IN_CGROUP; + + /* A start tag whose tag name is "col" */ + } elseif ($token['type'] === HTML5::STARTTAG && + $token['name'] === 'col' + ) { + $this->inTable( + array( + 'name' => 'colgroup', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + $this->inColumnGroup($token); + + /* A start tag whose tag name is one of: "tbody", "tfoot", "thead" */ + } elseif ($token['type'] === HTML5::STARTTAG && in_array( + $token['name'], + array('tbody', 'tfoot', 'thead') + ) + ) { + /* Clear the stack back to a table context. */ + $this->clearStackToTableContext($clear); + + /* Insert an HTML element for the token, then switch the insertion + mode to "in table body". */ + $this->insertElement($token); + $this->mode = self::IN_TBODY; + + /* A start tag whose tag name is one of: "td", "th", "tr" */ + } elseif ($token['type'] === HTML5::STARTTAG && + in_array($token['name'], array('td', 'th', 'tr')) + ) { + /* Act as if a start tag token with the tag name "tbody" had been + seen, then reprocess the current token. */ + $this->inTable( + array( + 'name' => 'tbody', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + return $this->inTableBody($token); + + /* A start tag whose tag name is "table" */ + } elseif ($token['type'] === HTML5::STARTTAG && + $token['name'] === 'table' + ) { + /* Parse error. Act as if an end tag token with the tag name "table" + had been seen, then, if that token wasn't ignored, reprocess the + current token. */ + $this->inTable( + array( + 'name' => 'table', + 'type' => HTML5::ENDTAG + ) + ); + + return $this->mainPhase($token); + + /* An end tag whose tag name is "table" */ + } elseif ($token['type'] === HTML5::ENDTAG && + $token['name'] === 'table' + ) { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. (innerHTML case) */ + if (!$this->elementInScope($token['name'], true)) { + return false; + + /* Otherwise: */ + } else { + /* Generate implied end tags. */ + $this->generateImpliedEndTags(); + + /* Now, if the current node is not a table element, then this + is a parse error. */ + // w/e + + /* Pop elements from this stack until a table element has been + popped from the stack. */ + while (true) { + $current = end($this->stack)->nodeName; + array_pop($this->stack); + + if ($current === 'table') { + break; + } + } + + /* Reset the insertion mode appropriately. */ + $this->resetInsertionMode(); + } + + /* An end tag whose tag name is one of: "body", "caption", "col", + "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr" */ + } elseif ($token['type'] === HTML5::ENDTAG && in_array( + $token['name'], + array( + 'body', + 'caption', + 'col', + 'colgroup', + 'html', + 'tbody', + 'td', + 'tfoot', + 'th', + 'thead', + 'tr' + ) + ) + ) { + // Parse error. Ignore the token. + + /* Anything else */ + } else { + /* Parse error. Process the token as if the insertion mode was "in + body", with the following exception: */ + + /* If the current node is a table, tbody, tfoot, thead, or tr + element, then, whenever a node would be inserted into the current + node, it must instead be inserted into the foster parent element. */ + if (in_array( + end($this->stack)->nodeName, + array('table', 'tbody', 'tfoot', 'thead', 'tr') + ) + ) { + /* The foster parent element is the parent element of the last + table element in the stack of open elements, if there is a + table element and it has such a parent element. If there is no + table element in the stack of open elements (innerHTML case), + then the foster parent element is the first element in the + stack of open elements (the html element). Otherwise, if there + is a table element in the stack of open elements, but the last + table element in the stack of open elements has no parent, or + its parent node is not an element, then the foster parent + element is the element before the last table element in the + stack of open elements. */ + for ($n = count($this->stack) - 1; $n >= 0; $n--) { + if ($this->stack[$n]->nodeName === 'table') { + $table = $this->stack[$n]; + break; + } + } + + if (isset($table) && $table->parentNode !== null) { + $this->foster_parent = $table->parentNode; + + } elseif (!isset($table)) { + $this->foster_parent = $this->stack[0]; + + } elseif (isset($table) && ($table->parentNode === null || + $table->parentNode->nodeType !== XML_ELEMENT_NODE) + ) { + $this->foster_parent = $this->stack[$n - 1]; + } + } + + $this->inBody($token); + } + } + + private function inCaption($token) + { + /* An end tag whose tag name is "caption" */ + if ($token['type'] === HTML5::ENDTAG && $token['name'] === 'caption') { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. (innerHTML case) */ + if (!$this->elementInScope($token['name'], true)) { + // Ignore + + /* Otherwise: */ + } else { + /* Generate implied end tags. */ + $this->generateImpliedEndTags(); + + /* Now, if the current node is not a caption element, then this + is a parse error. */ + // w/e + + /* Pop elements from this stack until a caption element has + been popped from the stack. */ + while (true) { + $node = end($this->stack)->nodeName; + array_pop($this->stack); + + if ($node === 'caption') { + break; + } + } + + /* Clear the list of active formatting elements up to the last + marker. */ + $this->clearTheActiveFormattingElementsUpToTheLastMarker(); + + /* Switch the insertion mode to "in table". */ + $this->mode = self::IN_TABLE; + } + + /* A start tag whose tag name is one of: "caption", "col", "colgroup", + "tbody", "td", "tfoot", "th", "thead", "tr", or an end tag whose tag + name is "table" */ + } elseif (($token['type'] === HTML5::STARTTAG && in_array( + $token['name'], + array( + 'caption', + 'col', + 'colgroup', + 'tbody', + 'td', + 'tfoot', + 'th', + 'thead', + 'tr' + ) + )) || ($token['type'] === HTML5::ENDTAG && + $token['name'] === 'table') + ) { + /* Parse error. Act as if an end tag with the tag name "caption" + had been seen, then, if that token wasn't ignored, reprocess the + current token. */ + $this->inCaption( + array( + 'name' => 'caption', + 'type' => HTML5::ENDTAG + ) + ); + + return $this->inTable($token); + + /* An end tag whose tag name is one of: "body", "col", "colgroup", + "html", "tbody", "td", "tfoot", "th", "thead", "tr" */ + } elseif ($token['type'] === HTML5::ENDTAG && in_array( + $token['name'], + array( + 'body', + 'col', + 'colgroup', + 'html', + 'tbody', + 'tfoot', + 'th', + 'thead', + 'tr' + ) + ) + ) { + // Parse error. Ignore the token. + + /* Anything else */ + } else { + /* Process the token as if the insertion mode was "in body". */ + $this->inBody($token); + } + } + + private function inColumnGroup($token) + { + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + if ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Append the character to the current node. */ + $text = $this->dom->createTextNode($token['data']); + end($this->stack)->appendChild($text); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $comment = $this->dom->createComment($token['data']); + end($this->stack)->appendChild($comment); + + /* A start tag whose tag name is "col" */ + } elseif ($token['type'] === HTML5::STARTTAG && $token['name'] === 'col') { + /* Insert a col element for the token. Immediately pop the current + node off the stack of open elements. */ + $this->insertElement($token); + array_pop($this->stack); + + /* An end tag whose tag name is "colgroup" */ + } elseif ($token['type'] === HTML5::ENDTAG && + $token['name'] === 'colgroup' + ) { + /* If the current node is the root html element, then this is a + parse error, ignore the token. (innerHTML case) */ + if (end($this->stack)->nodeName === 'html') { + // Ignore + + /* Otherwise, pop the current node (which will be a colgroup + element) from the stack of open elements. Switch the insertion + mode to "in table". */ + } else { + array_pop($this->stack); + $this->mode = self::IN_TABLE; + } + + /* An end tag whose tag name is "col" */ + } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'col') { + /* Parse error. Ignore the token. */ + + /* Anything else */ + } else { + /* Act as if an end tag with the tag name "colgroup" had been seen, + and then, if that token wasn't ignored, reprocess the current token. */ + $this->inColumnGroup( + array( + 'name' => 'colgroup', + 'type' => HTML5::ENDTAG + ) + ); + + return $this->inTable($token); + } + } + + private function inTableBody($token) + { + $clear = array('tbody', 'tfoot', 'thead', 'html'); + + /* A start tag whose tag name is "tr" */ + if ($token['type'] === HTML5::STARTTAG && $token['name'] === 'tr') { + /* Clear the stack back to a table body context. */ + $this->clearStackToTableContext($clear); + + /* Insert a tr element for the token, then switch the insertion + mode to "in row". */ + $this->insertElement($token); + $this->mode = self::IN_ROW; + + /* A start tag whose tag name is one of: "th", "td" */ + } elseif ($token['type'] === HTML5::STARTTAG && + ($token['name'] === 'th' || $token['name'] === 'td') + ) { + /* Parse error. Act as if a start tag with the tag name "tr" had + been seen, then reprocess the current token. */ + $this->inTableBody( + array( + 'name' => 'tr', + 'type' => HTML5::STARTTAG, + 'attr' => array() + ) + ); + + return $this->inRow($token); + + /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */ + } elseif ($token['type'] === HTML5::ENDTAG && + in_array($token['name'], array('tbody', 'tfoot', 'thead')) + ) { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. */ + if (!$this->elementInScope($token['name'], true)) { + // Ignore + + /* Otherwise: */ + } else { + /* Clear the stack back to a table body context. */ + $this->clearStackToTableContext($clear); + + /* Pop the current node from the stack of open elements. Switch + the insertion mode to "in table". */ + array_pop($this->stack); + $this->mode = self::IN_TABLE; + } + + /* A start tag whose tag name is one of: "caption", "col", "colgroup", + "tbody", "tfoot", "thead", or an end tag whose tag name is "table" */ + } elseif (($token['type'] === HTML5::STARTTAG && in_array( + $token['name'], + array('caption', 'col', 'colgroup', 'tbody', 'tfoor', 'thead') + )) || + ($token['type'] === HTML5::STARTTAG && $token['name'] === 'table') + ) { + /* If the stack of open elements does not have a tbody, thead, or + tfoot element in table scope, this is a parse error. Ignore the + token. (innerHTML case) */ + if (!$this->elementInScope(array('tbody', 'thead', 'tfoot'), true)) { + // Ignore. + + /* Otherwise: */ + } else { + /* Clear the stack back to a table body context. */ + $this->clearStackToTableContext($clear); + + /* Act as if an end tag with the same tag name as the current + node ("tbody", "tfoot", or "thead") had been seen, then + reprocess the current token. */ + $this->inTableBody( + array( + 'name' => end($this->stack)->nodeName, + 'type' => HTML5::ENDTAG + ) + ); + + return $this->mainPhase($token); + } + + /* An end tag whose tag name is one of: "body", "caption", "col", + "colgroup", "html", "td", "th", "tr" */ + } elseif ($token['type'] === HTML5::ENDTAG && in_array( + $token['name'], + array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr') + ) + ) { + /* Parse error. Ignore the token. */ + + /* Anything else */ + } else { + /* Process the token as if the insertion mode was "in table". */ + $this->inTable($token); + } + } + + private function inRow($token) + { + $clear = array('tr', 'html'); + + /* A start tag whose tag name is one of: "th", "td" */ + if ($token['type'] === HTML5::STARTTAG && + ($token['name'] === 'th' || $token['name'] === 'td') + ) { + /* Clear the stack back to a table row context. */ + $this->clearStackToTableContext($clear); + + /* Insert an HTML element for the token, then switch the insertion + mode to "in cell". */ + $this->insertElement($token); + $this->mode = self::IN_CELL; + + /* Insert a marker at the end of the list of active formatting + elements. */ + $this->a_formatting[] = self::MARKER; + + /* An end tag whose tag name is "tr" */ + } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'tr') { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. (innerHTML case) */ + if (!$this->elementInScope($token['name'], true)) { + // Ignore. + + /* Otherwise: */ + } else { + /* Clear the stack back to a table row context. */ + $this->clearStackToTableContext($clear); + + /* Pop the current node (which will be a tr element) from the + stack of open elements. Switch the insertion mode to "in table + body". */ + array_pop($this->stack); + $this->mode = self::IN_TBODY; + } + + /* A start tag whose tag name is one of: "caption", "col", "colgroup", + "tbody", "tfoot", "thead", "tr" or an end tag whose tag name is "table" */ + } elseif ($token['type'] === HTML5::STARTTAG && in_array( + $token['name'], + array('caption', 'col', 'colgroup', 'tbody', 'tfoot', 'thead', 'tr') + ) + ) { + /* Act as if an end tag with the tag name "tr" had been seen, then, + if that token wasn't ignored, reprocess the current token. */ + $this->inRow( + array( + 'name' => 'tr', + 'type' => HTML5::ENDTAG + ) + ); + + return $this->inCell($token); + + /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */ + } elseif ($token['type'] === HTML5::ENDTAG && + in_array($token['name'], array('tbody', 'tfoot', 'thead')) + ) { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. */ + if (!$this->elementInScope($token['name'], true)) { + // Ignore. + + /* Otherwise: */ + } else { + /* Otherwise, act as if an end tag with the tag name "tr" had + been seen, then reprocess the current token. */ + $this->inRow( + array( + 'name' => 'tr', + 'type' => HTML5::ENDTAG + ) + ); + + return $this->inCell($token); + } + + /* An end tag whose tag name is one of: "body", "caption", "col", + "colgroup", "html", "td", "th" */ + } elseif ($token['type'] === HTML5::ENDTAG && in_array( + $token['name'], + array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr') + ) + ) { + /* Parse error. Ignore the token. */ + + /* Anything else */ + } else { + /* Process the token as if the insertion mode was "in table". */ + $this->inTable($token); + } + } + + private function inCell($token) + { + /* An end tag whose tag name is one of: "td", "th" */ + if ($token['type'] === HTML5::ENDTAG && + ($token['name'] === 'td' || $token['name'] === 'th') + ) { + /* If the stack of open elements does not have an element in table + scope with the same tag name as that of the token, then this is a + parse error and the token must be ignored. */ + if (!$this->elementInScope($token['name'], true)) { + // Ignore. + + /* Otherwise: */ + } else { + /* Generate implied end tags, except for elements with the same + tag name as the token. */ + $this->generateImpliedEndTags(array($token['name'])); + + /* Now, if the current node is not an element with the same tag + name as the token, then this is a parse error. */ + // k + + /* Pop elements from this stack until an element with the same + tag name as the token has been popped from the stack. */ + while (true) { + $node = end($this->stack)->nodeName; + array_pop($this->stack); + + if ($node === $token['name']) { + break; + } + } + + /* Clear the list of active formatting elements up to the last + marker. */ + $this->clearTheActiveFormattingElementsUpToTheLastMarker(); + + /* Switch the insertion mode to "in row". (The current node + will be a tr element at this point.) */ + $this->mode = self::IN_ROW; + } + + /* A start tag whose tag name is one of: "caption", "col", "colgroup", + "tbody", "td", "tfoot", "th", "thead", "tr" */ + } elseif ($token['type'] === HTML5::STARTTAG && in_array( + $token['name'], + array( + 'caption', + 'col', + 'colgroup', + 'tbody', + 'td', + 'tfoot', + 'th', + 'thead', + 'tr' + ) + ) + ) { + /* If the stack of open elements does not have a td or th element + in table scope, then this is a parse error; ignore the token. + (innerHTML case) */ + if (!$this->elementInScope(array('td', 'th'), true)) { + // Ignore. + + /* Otherwise, close the cell (see below) and reprocess the current + token. */ + } else { + $this->closeCell(); + return $this->inRow($token); + } + + /* A start tag whose tag name is one of: "caption", "col", "colgroup", + "tbody", "td", "tfoot", "th", "thead", "tr" */ + } elseif ($token['type'] === HTML5::STARTTAG && in_array( + $token['name'], + array( + 'caption', + 'col', + 'colgroup', + 'tbody', + 'td', + 'tfoot', + 'th', + 'thead', + 'tr' + ) + ) + ) { + /* If the stack of open elements does not have a td or th element + in table scope, then this is a parse error; ignore the token. + (innerHTML case) */ + if (!$this->elementInScope(array('td', 'th'), true)) { + // Ignore. + + /* Otherwise, close the cell (see below) and reprocess the current + token. */ + } else { + $this->closeCell(); + return $this->inRow($token); + } + + /* An end tag whose tag name is one of: "body", "caption", "col", + "colgroup", "html" */ + } elseif ($token['type'] === HTML5::ENDTAG && in_array( + $token['name'], + array('body', 'caption', 'col', 'colgroup', 'html') + ) + ) { + /* Parse error. Ignore the token. */ + + /* An end tag whose tag name is one of: "table", "tbody", "tfoot", + "thead", "tr" */ + } elseif ($token['type'] === HTML5::ENDTAG && in_array( + $token['name'], + array('table', 'tbody', 'tfoot', 'thead', 'tr') + ) + ) { + /* If the stack of open elements does not have an element in table + scope with the same tag name as that of the token (which can only + happen for "tbody", "tfoot" and "thead", or, in the innerHTML case), + then this is a parse error and the token must be ignored. */ + if (!$this->elementInScope($token['name'], true)) { + // Ignore. + + /* Otherwise, close the cell (see below) and reprocess the current + token. */ + } else { + $this->closeCell(); + return $this->inRow($token); + } + + /* Anything else */ + } else { + /* Process the token as if the insertion mode was "in body". */ + $this->inBody($token); + } + } + + private function inSelect($token) + { + /* Handle the token as follows: */ + + /* A character token */ + if ($token['type'] === HTML5::CHARACTR) { + /* Append the token's character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $this->insertComment($token['data']); + + /* A start tag token whose tag name is "option" */ + } elseif ($token['type'] === HTML5::STARTTAG && + $token['name'] === 'option' + ) { + /* If the current node is an option element, act as if an end tag + with the tag name "option" had been seen. */ + if (end($this->stack)->nodeName === 'option') { + $this->inSelect( + array( + 'name' => 'option', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* A start tag token whose tag name is "optgroup" */ + } elseif ($token['type'] === HTML5::STARTTAG && + $token['name'] === 'optgroup' + ) { + /* If the current node is an option element, act as if an end tag + with the tag name "option" had been seen. */ + if (end($this->stack)->nodeName === 'option') { + $this->inSelect( + array( + 'name' => 'option', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* If the current node is an optgroup element, act as if an end tag + with the tag name "optgroup" had been seen. */ + if (end($this->stack)->nodeName === 'optgroup') { + $this->inSelect( + array( + 'name' => 'optgroup', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* An end tag token whose tag name is "optgroup" */ + } elseif ($token['type'] === HTML5::ENDTAG && + $token['name'] === 'optgroup' + ) { + /* First, if the current node is an option element, and the node + immediately before it in the stack of open elements is an optgroup + element, then act as if an end tag with the tag name "option" had + been seen. */ + $elements_in_stack = count($this->stack); + + if ($this->stack[$elements_in_stack - 1]->nodeName === 'option' && + $this->stack[$elements_in_stack - 2]->nodeName === 'optgroup' + ) { + $this->inSelect( + array( + 'name' => 'option', + 'type' => HTML5::ENDTAG + ) + ); + } + + /* If the current node is an optgroup element, then pop that node + from the stack of open elements. Otherwise, this is a parse error, + ignore the token. */ + if ($this->stack[$elements_in_stack - 1] === 'optgroup') { + array_pop($this->stack); + } + + /* An end tag token whose tag name is "option" */ + } elseif ($token['type'] === HTML5::ENDTAG && + $token['name'] === 'option' + ) { + /* If the current node is an option element, then pop that node + from the stack of open elements. Otherwise, this is a parse error, + ignore the token. */ + if (end($this->stack)->nodeName === 'option') { + array_pop($this->stack); + } + + /* An end tag whose tag name is "select" */ + } elseif ($token['type'] === HTML5::ENDTAG && + $token['name'] === 'select' + ) { + /* If the stack of open elements does not have an element in table + scope with the same tag name as the token, this is a parse error. + Ignore the token. (innerHTML case) */ + if (!$this->elementInScope($token['name'], true)) { + // w/e + + /* Otherwise: */ + } else { + /* Pop elements from the stack of open elements until a select + element has been popped from the stack. */ + while (true) { + $current = end($this->stack)->nodeName; + array_pop($this->stack); + + if ($current === 'select') { + break; + } + } + + /* Reset the insertion mode appropriately. */ + $this->resetInsertionMode(); + } + + /* A start tag whose tag name is "select" */ + } elseif ($token['name'] === 'select' && + $token['type'] === HTML5::STARTTAG + ) { + /* Parse error. Act as if the token had been an end tag with the + tag name "select" instead. */ + $this->inSelect( + array( + 'name' => 'select', + 'type' => HTML5::ENDTAG + ) + ); + + /* An end tag whose tag name is one of: "caption", "table", "tbody", + "tfoot", "thead", "tr", "td", "th" */ + } elseif (in_array( + $token['name'], + array( + 'caption', + 'table', + 'tbody', + 'tfoot', + 'thead', + 'tr', + 'td', + 'th' + ) + ) && $token['type'] === HTML5::ENDTAG + ) { + /* Parse error. */ + // w/e + + /* If the stack of open elements has an element in table scope with + the same tag name as that of the token, then act as if an end tag + with the tag name "select" had been seen, and reprocess the token. + Otherwise, ignore the token. */ + if ($this->elementInScope($token['name'], true)) { + $this->inSelect( + array( + 'name' => 'select', + 'type' => HTML5::ENDTAG + ) + ); + + $this->mainPhase($token); + } + + /* Anything else */ + } else { + /* Parse error. Ignore the token. */ + } + } + + private function afterBody($token) + { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + if ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Process the token as it would be processed if the insertion mode + was "in body". */ + $this->inBody($token); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the first element in the stack of open + elements (the html element), with the data attribute set to the + data given in the comment token. */ + $comment = $this->dom->createComment($token['data']); + $this->stack[0]->appendChild($comment); + + /* An end tag with the tag name "html" */ + } elseif ($token['type'] === HTML5::ENDTAG && $token['name'] === 'html') { + /* If the parser was originally created in order to handle the + setting of an element's innerHTML attribute, this is a parse error; + ignore the token. (The element will be an html element in this + case.) (innerHTML case) */ + + /* Otherwise, switch to the trailing end phase. */ + $this->phase = self::END_PHASE; + + /* Anything else */ + } else { + /* Parse error. Set the insertion mode to "in body" and reprocess + the token. */ + $this->mode = self::IN_BODY; + return $this->inBody($token); + } + } + + private function inFrameset($token) + { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */ + if ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Append the character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $this->insertComment($token['data']); + + /* A start tag with the tag name "frameset" */ + } elseif ($token['name'] === 'frameset' && + $token['type'] === HTML5::STARTTAG + ) { + $this->insertElement($token); + + /* An end tag with the tag name "frameset" */ + } elseif ($token['name'] === 'frameset' && + $token['type'] === HTML5::ENDTAG + ) { + /* If the current node is the root html element, then this is a + parse error; ignore the token. (innerHTML case) */ + if (end($this->stack)->nodeName === 'html') { + // Ignore + + } else { + /* Otherwise, pop the current node from the stack of open + elements. */ + array_pop($this->stack); + + /* If the parser was not originally created in order to handle + the setting of an element's innerHTML attribute (innerHTML case), + and the current node is no longer a frameset element, then change + the insertion mode to "after frameset". */ + $this->mode = self::AFTR_FRAME; + } + + /* A start tag with the tag name "frame" */ + } elseif ($token['name'] === 'frame' && + $token['type'] === HTML5::STARTTAG + ) { + /* Insert an HTML element for the token. */ + $this->insertElement($token); + + /* Immediately pop the current node off the stack of open elements. */ + array_pop($this->stack); + + /* A start tag with the tag name "noframes" */ + } elseif ($token['name'] === 'noframes' && + $token['type'] === HTML5::STARTTAG + ) { + /* Process the token as if the insertion mode had been "in body". */ + $this->inBody($token); + + /* Anything else */ + } else { + /* Parse error. Ignore the token. */ + } + } + + private function afterFrameset($token) + { + /* Handle the token as follows: */ + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */ + if ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Append the character to the current node. */ + $this->insertText($token['data']); + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the current node with the data + attribute set to the data given in the comment token. */ + $this->insertComment($token['data']); + + /* An end tag with the tag name "html" */ + } elseif ($token['name'] === 'html' && + $token['type'] === HTML5::ENDTAG + ) { + /* Switch to the trailing end phase. */ + $this->phase = self::END_PHASE; + + /* A start tag with the tag name "noframes" */ + } elseif ($token['name'] === 'noframes' && + $token['type'] === HTML5::STARTTAG + ) { + /* Process the token as if the insertion mode had been "in body". */ + $this->inBody($token); + + /* Anything else */ + } else { + /* Parse error. Ignore the token. */ + } + } + + private function trailingEndPhase($token) + { + /* After the main phase, as each token is emitted from the tokenisation + stage, it must be processed as described in this section. */ + + /* A DOCTYPE token */ + if ($token['type'] === HTML5::DOCTYPE) { + // Parse error. Ignore the token. + + /* A comment token */ + } elseif ($token['type'] === HTML5::COMMENT) { + /* Append a Comment node to the Document object with the data + attribute set to the data given in the comment token. */ + $comment = $this->dom->createComment($token['data']); + $this->dom->appendChild($comment); + + /* A character token that is one of one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE */ + } elseif ($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']) + ) { + /* Process the token as it would be processed in the main phase. */ + $this->mainPhase($token); + + /* A character token that is not one of U+0009 CHARACTER TABULATION, + U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), + or U+0020 SPACE. Or a start tag token. Or an end tag token. */ + } elseif (($token['type'] === HTML5::CHARACTR && + preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || + $token['type'] === HTML5::STARTTAG || $token['type'] === HTML5::ENDTAG + ) { + /* Parse error. Switch back to the main phase and reprocess the + token. */ + $this->phase = self::MAIN_PHASE; + return $this->mainPhase($token); + + /* An end-of-file token */ + } elseif ($token['type'] === HTML5::EOF) { + /* OMG DONE!! */ + } + } + + private function insertElement($token, $append = true, $check = false) + { + // Proprietary workaround for libxml2's limitations with tag names + if ($check) { + // Slightly modified HTML5 tag-name modification, + // removing anything that's not an ASCII letter, digit, or hyphen + $token['name'] = preg_replace('/[^a-z0-9-]/i', '', $token['name']); + // Remove leading hyphens and numbers + $token['name'] = ltrim($token['name'], '-0..9'); + // In theory, this should ever be needed, but just in case + if ($token['name'] === '') { + $token['name'] = 'span'; + } // arbitrary generic choice + } + + $el = $this->dom->createElement($token['name']); + + foreach ($token['attr'] as $attr) { + if (!$el->hasAttribute($attr['name'])) { + $el->setAttribute($attr['name'], $attr['value']); + } + } + + $this->appendToRealParent($el); + $this->stack[] = $el; + + return $el; + } + + private function insertText($data) + { + $text = $this->dom->createTextNode($data); + $this->appendToRealParent($text); + } + + private function insertComment($data) + { + $comment = $this->dom->createComment($data); + $this->appendToRealParent($comment); + } + + private function appendToRealParent($node) + { + if ($this->foster_parent === null) { + end($this->stack)->appendChild($node); + + } elseif ($this->foster_parent !== null) { + /* If the foster parent element is the parent element of the + last table element in the stack of open elements, then the new + node must be inserted immediately before the last table element + in the stack of open elements in the foster parent element; + otherwise, the new node must be appended to the foster parent + element. */ + for ($n = count($this->stack) - 1; $n >= 0; $n--) { + if ($this->stack[$n]->nodeName === 'table' && + $this->stack[$n]->parentNode !== null + ) { + $table = $this->stack[$n]; + break; + } + } + + if (isset($table) && $this->foster_parent->isSameNode($table->parentNode)) { + $this->foster_parent->insertBefore($node, $table); + } else { + $this->foster_parent->appendChild($node); + } + + $this->foster_parent = null; + } + } + + private function elementInScope($el, $table = false) + { + if (is_array($el)) { + foreach ($el as $element) { + if ($this->elementInScope($element, $table)) { + return true; + } + } + + return false; + } + + $leng = count($this->stack); + + for ($n = 0; $n < $leng; $n++) { + /* 1. Initialise node to be the current node (the bottommost node of + the stack). */ + $node = $this->stack[$leng - 1 - $n]; + + if ($node->tagName === $el) { + /* 2. If node is the target node, terminate in a match state. */ + return true; + + } elseif ($node->tagName === 'table') { + /* 3. Otherwise, if node is a table element, terminate in a failure + state. */ + return false; + + } elseif ($table === true && in_array( + $node->tagName, + array( + 'caption', + 'td', + 'th', + 'button', + 'marquee', + 'object' + ) + ) + ) { + /* 4. Otherwise, if the algorithm is the "has an element in scope" + variant (rather than the "has an element in table scope" variant), + and node is one of the following, terminate in a failure state. */ + return false; + + } elseif ($node === $node->ownerDocument->documentElement) { + /* 5. Otherwise, if node is an html element (root element), terminate + in a failure state. (This can only happen if the node is the topmost + node of the stack of open elements, and prevents the next step from + being invoked if there are no more elements in the stack.) */ + return false; + } + + /* Otherwise, set node to the previous entry in the stack of open + elements and return to step 2. (This will never fail, since the loop + will always terminate in the previous step if the top of the stack + is reached.) */ + } + } + + private function reconstructActiveFormattingElements() + { + /* 1. If there are no entries in the list of active formatting elements, + then there is nothing to reconstruct; stop this algorithm. */ + $formatting_elements = count($this->a_formatting); + + if ($formatting_elements === 0) { + return false; + } + + /* 3. Let entry be the last (most recently added) element in the list + of active formatting elements. */ + $entry = end($this->a_formatting); + + /* 2. If the last (most recently added) entry in the list of active + formatting elements is a marker, or if it is an element that is in the + stack of open elements, then there is nothing to reconstruct; stop this + algorithm. */ + if ($entry === self::MARKER || in_array($entry, $this->stack, true)) { + return false; + } + + for ($a = $formatting_elements - 1; $a >= 0; true) { + /* 4. If there are no entries before entry in the list of active + formatting elements, then jump to step 8. */ + if ($a === 0) { + $step_seven = false; + break; + } + + /* 5. Let entry be the entry one earlier than entry in the list of + active formatting elements. */ + $a--; + $entry = $this->a_formatting[$a]; + + /* 6. If entry is neither a marker nor an element that is also in + thetack of open elements, go to step 4. */ + if ($entry === self::MARKER || in_array($entry, $this->stack, true)) { + break; + } + } + + while (true) { + /* 7. Let entry be the element one later than entry in the list of + active formatting elements. */ + if (isset($step_seven) && $step_seven === true) { + $a++; + $entry = $this->a_formatting[$a]; + } + + /* 8. Perform a shallow clone of the element entry to obtain clone. */ + $clone = $entry->cloneNode(); + + /* 9. Append clone to the current node and push it onto the stack + of open elements so that it is the new current node. */ + end($this->stack)->appendChild($clone); + $this->stack[] = $clone; + + /* 10. Replace the entry for entry in the list with an entry for + clone. */ + $this->a_formatting[$a] = $clone; + + /* 11. If the entry for clone in the list of active formatting + elements is not the last entry in the list, return to step 7. */ + if (end($this->a_formatting) !== $clone) { + $step_seven = true; + } else { + break; + } + } + } + + private function clearTheActiveFormattingElementsUpToTheLastMarker() + { + /* When the steps below require the UA to clear the list of active + formatting elements up to the last marker, the UA must perform the + following steps: */ + + while (true) { + /* 1. Let entry be the last (most recently added) entry in the list + of active formatting elements. */ + $entry = end($this->a_formatting); + + /* 2. Remove entry from the list of active formatting elements. */ + array_pop($this->a_formatting); + + /* 3. If entry was a marker, then stop the algorithm at this point. + The list has been cleared up to the last marker. */ + if ($entry === self::MARKER) { + break; + } + } + } + + private function generateImpliedEndTags($exclude = array()) + { + /* When the steps below require the UA to generate implied end tags, + then, if the current node is a dd element, a dt element, an li element, + a p element, a td element, a th element, or a tr element, the UA must + act as if an end tag with the respective tag name had been seen and + then generate implied end tags again. */ + $node = end($this->stack); + $elements = array_diff(array('dd', 'dt', 'li', 'p', 'td', 'th', 'tr'), $exclude); + + while (in_array(end($this->stack)->nodeName, $elements)) { + array_pop($this->stack); + } + } + + private function getElementCategory($node) + { + $name = $node->tagName; + if (in_array($name, $this->special)) { + return self::SPECIAL; + } elseif (in_array($name, $this->scoping)) { + return self::SCOPING; + } elseif (in_array($name, $this->formatting)) { + return self::FORMATTING; + } else { + return self::PHRASING; + } + } + + private function clearStackToTableContext($elements) + { + /* When the steps above require the UA to clear the stack back to a + table context, it means that the UA must, while the current node is not + a table element or an html element, pop elements from the stack of open + elements. If this causes any elements to be popped from the stack, then + this is a parse error. */ + while (true) { + $node = end($this->stack)->nodeName; + + if (in_array($node, $elements)) { + break; + } else { + array_pop($this->stack); + } + } + } + + private function resetInsertionMode() + { + /* 1. Let last be false. */ + $last = false; + $leng = count($this->stack); + + for ($n = $leng - 1; $n >= 0; $n--) { + /* 2. Let node be the last node in the stack of open elements. */ + $node = $this->stack[$n]; + + /* 3. If node is the first node in the stack of open elements, then + set last to true. If the element whose innerHTML attribute is being + set is neither a td element nor a th element, then set node to the + element whose innerHTML attribute is being set. (innerHTML case) */ + if ($this->stack[0]->isSameNode($node)) { + $last = true; + } + + /* 4. If node is a select element, then switch the insertion mode to + "in select" and abort these steps. (innerHTML case) */ + if ($node->nodeName === 'select') { + $this->mode = self::IN_SELECT; + break; + + /* 5. If node is a td or th element, then switch the insertion mode + to "in cell" and abort these steps. */ + } elseif ($node->nodeName === 'td' || $node->nodeName === 'th') { + $this->mode = self::IN_CELL; + break; + + /* 6. If node is a tr element, then switch the insertion mode to + "in row" and abort these steps. */ + } elseif ($node->nodeName === 'tr') { + $this->mode = self::IN_ROW; + break; + + /* 7. If node is a tbody, thead, or tfoot element, then switch the + insertion mode to "in table body" and abort these steps. */ + } elseif (in_array($node->nodeName, array('tbody', 'thead', 'tfoot'))) { + $this->mode = self::IN_TBODY; + break; + + /* 8. If node is a caption element, then switch the insertion mode + to "in caption" and abort these steps. */ + } elseif ($node->nodeName === 'caption') { + $this->mode = self::IN_CAPTION; + break; + + /* 9. If node is a colgroup element, then switch the insertion mode + to "in column group" and abort these steps. (innerHTML case) */ + } elseif ($node->nodeName === 'colgroup') { + $this->mode = self::IN_CGROUP; + break; + + /* 10. If node is a table element, then switch the insertion mode + to "in table" and abort these steps. */ + } elseif ($node->nodeName === 'table') { + $this->mode = self::IN_TABLE; + break; + + /* 11. If node is a head element, then switch the insertion mode + to "in body" ("in body"! not "in head"!) and abort these steps. + (innerHTML case) */ + } elseif ($node->nodeName === 'head') { + $this->mode = self::IN_BODY; + break; + + /* 12. If node is a body element, then switch the insertion mode to + "in body" and abort these steps. */ + } elseif ($node->nodeName === 'body') { + $this->mode = self::IN_BODY; + break; + + /* 13. If node is a frameset element, then switch the insertion + mode to "in frameset" and abort these steps. (innerHTML case) */ + } elseif ($node->nodeName === 'frameset') { + $this->mode = self::IN_FRAME; + break; + + /* 14. If node is an html element, then: if the head element + pointer is null, switch the insertion mode to "before head", + otherwise, switch the insertion mode to "after head". In either + case, abort these steps. (innerHTML case) */ + } elseif ($node->nodeName === 'html') { + $this->mode = ($this->head_pointer === null) + ? self::BEFOR_HEAD + : self::AFTER_HEAD; + + break; + + /* 15. If last is true, then set the insertion mode to "in body" + and abort these steps. (innerHTML case) */ + } elseif ($last) { + $this->mode = self::IN_BODY; + break; + } + } + } + + private function closeCell() + { + /* If the stack of open elements has a td or th element in table scope, + then act as if an end tag token with that tag name had been seen. */ + foreach (array('td', 'th') as $cell) { + if ($this->elementInScope($cell, true)) { + $this->inCell( + array( + 'name' => $cell, + 'type' => HTML5::ENDTAG + ) + ); + + break; + } + } + } + + public function save() + { + return $this->dom; + } +} diff --git a/lib/purifier/HTMLPurifier/Node.php b/lib/purifier/HTMLPurifier/Node.php index d7dcf62398..3995fec9fe 100644 --- a/lib/purifier/HTMLPurifier/Node.php +++ b/lib/purifier/HTMLPurifier/Node.php @@ -1,49 +1,49 @@ -data = $data; - $this->line = $line; - $this->col = $col; - } - - public function toTokenPair() { - return array(new HTMLPurifier_Token_Comment($this->data, $this->line, $this->col), null); - } -} +data = $data; + $this->line = $line; + $this->col = $col; + } + + public function toTokenPair() { + return array(new HTMLPurifier_Token_Comment($this->data, $this->line, $this->col), null); + } +} diff --git a/lib/purifier/HTMLPurifier/Node/Element.php b/lib/purifier/HTMLPurifier/Node/Element.php index 7db4d0253f..6cbf56dada 100644 --- a/lib/purifier/HTMLPurifier/Node/Element.php +++ b/lib/purifier/HTMLPurifier/Node/Element.php @@ -1,59 +1,59 @@ - form or the form, i.e. - * is it a pair of start/end tokens or an empty token. - * @bool - */ - public $empty = false; - - public $endCol = null, $endLine = null, $endArmor = array(); - - public function __construct($name, $attr = array(), $line = null, $col = null, $armor = array()) { - $this->name = $name; - $this->attr = $attr; - $this->line = $line; - $this->col = $col; - $this->armor = $armor; - } - - public function toTokenPair() { - // XXX inefficiency here, normalization is not necessary - if ($this->empty) { - return array(new HTMLPurifier_Token_Empty($this->name, $this->attr, $this->line, $this->col, $this->armor), null); - } else { - $start = new HTMLPurifier_Token_Start($this->name, $this->attr, $this->line, $this->col, $this->armor); - $end = new HTMLPurifier_Token_End($this->name, array(), $this->endLine, $this->endCol, $this->endArmor); - //$end->start = $start; - return array($start, $end); - } - } -} - + form or the form, i.e. + * is it a pair of start/end tokens or an empty token. + * @bool + */ + public $empty = false; + + public $endCol = null, $endLine = null, $endArmor = array(); + + public function __construct($name, $attr = array(), $line = null, $col = null, $armor = array()) { + $this->name = $name; + $this->attr = $attr; + $this->line = $line; + $this->col = $col; + $this->armor = $armor; + } + + public function toTokenPair() { + // XXX inefficiency here, normalization is not necessary + if ($this->empty) { + return array(new HTMLPurifier_Token_Empty($this->name, $this->attr, $this->line, $this->col, $this->armor), null); + } else { + $start = new HTMLPurifier_Token_Start($this->name, $this->attr, $this->line, $this->col, $this->armor); + $end = new HTMLPurifier_Token_End($this->name, array(), $this->endLine, $this->endCol, $this->endArmor); + //$end->start = $start; + return array($start, $end); + } + } +} + diff --git a/lib/purifier/HTMLPurifier/Node/Text.php b/lib/purifier/HTMLPurifier/Node/Text.php index f51d861af2..aec9166473 100644 --- a/lib/purifier/HTMLPurifier/Node/Text.php +++ b/lib/purifier/HTMLPurifier/Node/Text.php @@ -1,54 +1,54 @@ -data = $data; - $this->is_whitespace = $is_whitespace; - $this->line = $line; - $this->col = $col; - } - - public function toTokenPair() { - return array(new HTMLPurifier_Token_Text($this->data, $this->line, $this->col), null); - } -} - -// vim: et sw=4 sts=4 +data = $data; + $this->is_whitespace = $is_whitespace; + $this->line = $line; + $this->col = $col; + } + + public function toTokenPair() { + return array(new HTMLPurifier_Token_Text($this->data, $this->line, $this->col), null); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/PercentEncoder.php b/lib/purifier/HTMLPurifier/PercentEncoder.php index fb9fd1fc95..18c8bbb00a 100644 --- a/lib/purifier/HTMLPurifier/PercentEncoder.php +++ b/lib/purifier/HTMLPurifier/PercentEncoder.php @@ -1,111 +1,111 @@ -preserve[$i] = true; - } - for ($i = 65; $i <= 90; $i++) { // upper-case - $this->preserve[$i] = true; - } - for ($i = 97; $i <= 122; $i++) { // lower-case - $this->preserve[$i] = true; - } - $this->preserve[45] = true; // Dash - - $this->preserve[46] = true; // Period . - $this->preserve[95] = true; // Underscore _ - $this->preserve[126]= true; // Tilde ~ - - // extra letters not to escape - if ($preserve !== false) { - for ($i = 0, $c = strlen($preserve); $i < $c; $i++) { - $this->preserve[ord($preserve[$i])] = true; - } - } - } - - /** - * Our replacement for urlencode, it encodes all non-reserved characters, - * as well as any extra characters that were instructed to be preserved. - * @note - * Assumes that the string has already been normalized, making any - * and all percent escape sequences valid. Percents will not be - * re-escaped, regardless of their status in $preserve - * @param string $string String to be encoded - * @return string Encoded string. - */ - public function encode($string) - { - $ret = ''; - for ($i = 0, $c = strlen($string); $i < $c; $i++) { - if ($string[$i] !== '%' && !isset($this->preserve[$int = ord($string[$i])])) { - $ret .= '%' . sprintf('%02X', $int); - } else { - $ret .= $string[$i]; - } - } - return $ret; - } - - /** - * Fix up percent-encoding by decoding unreserved characters and normalizing. - * @warning This function is affected by $preserve, even though the - * usual desired behavior is for this not to preserve those - * characters. Be careful when reusing instances of PercentEncoder! - * @param string $string String to normalize - * @return string - */ - public function normalize($string) - { - if ($string == '') { - return ''; - } - $parts = explode('%', $string); - $ret = array_shift($parts); - foreach ($parts as $part) { - $length = strlen($part); - if ($length < 2) { - $ret .= '%25' . $part; - continue; - } - $encoding = substr($part, 0, 2); - $text = substr($part, 2); - if (!ctype_xdigit($encoding)) { - $ret .= '%25' . $part; - continue; - } - $int = hexdec($encoding); - if (isset($this->preserve[$int])) { - $ret .= chr($int) . $text; - continue; - } - $encoding = strtoupper($encoding); - $ret .= '%' . $encoding . $text; - } - return $ret; - } -} - -// vim: et sw=4 sts=4 +preserve[$i] = true; + } + for ($i = 65; $i <= 90; $i++) { // upper-case + $this->preserve[$i] = true; + } + for ($i = 97; $i <= 122; $i++) { // lower-case + $this->preserve[$i] = true; + } + $this->preserve[45] = true; // Dash - + $this->preserve[46] = true; // Period . + $this->preserve[95] = true; // Underscore _ + $this->preserve[126]= true; // Tilde ~ + + // extra letters not to escape + if ($preserve !== false) { + for ($i = 0, $c = strlen($preserve); $i < $c; $i++) { + $this->preserve[ord($preserve[$i])] = true; + } + } + } + + /** + * Our replacement for urlencode, it encodes all non-reserved characters, + * as well as any extra characters that were instructed to be preserved. + * @note + * Assumes that the string has already been normalized, making any + * and all percent escape sequences valid. Percents will not be + * re-escaped, regardless of their status in $preserve + * @param string $string String to be encoded + * @return string Encoded string. + */ + public function encode($string) + { + $ret = ''; + for ($i = 0, $c = strlen($string); $i < $c; $i++) { + if ($string[$i] !== '%' && !isset($this->preserve[$int = ord($string[$i])])) { + $ret .= '%' . sprintf('%02X', $int); + } else { + $ret .= $string[$i]; + } + } + return $ret; + } + + /** + * Fix up percent-encoding by decoding unreserved characters and normalizing. + * @warning This function is affected by $preserve, even though the + * usual desired behavior is for this not to preserve those + * characters. Be careful when reusing instances of PercentEncoder! + * @param string $string String to normalize + * @return string + */ + public function normalize($string) + { + if ($string == '') { + return ''; + } + $parts = explode('%', $string); + $ret = array_shift($parts); + foreach ($parts as $part) { + $length = strlen($part); + if ($length < 2) { + $ret .= '%25' . $part; + continue; + } + $encoding = substr($part, 0, 2); + $text = substr($part, 2); + if (!ctype_xdigit($encoding)) { + $ret .= '%25' . $part; + continue; + } + $int = hexdec($encoding); + if (isset($this->preserve[$int])) { + $ret .= chr($int) . $text; + continue; + } + $encoding = strtoupper($encoding); + $ret .= '%' . $encoding . $text; + } + return $ret; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/Printer.php b/lib/purifier/HTMLPurifier/Printer.php index 16acd41574..549e4cea1e 100644 --- a/lib/purifier/HTMLPurifier/Printer.php +++ b/lib/purifier/HTMLPurifier/Printer.php @@ -1,218 +1,218 @@ -getAll(); - $context = new HTMLPurifier_Context(); - $this->generator = new HTMLPurifier_Generator($config, $context); - } - - /** - * Main function that renders object or aspect of that object - * @note Parameters vary depending on printer - */ - // function render() {} - - /** - * Returns a start tag - * @param string $tag Tag name - * @param array $attr Attribute array - * @return string - */ - protected function start($tag, $attr = array()) - { - return $this->generator->generateFromToken( - new HTMLPurifier_Token_Start($tag, $attr ? $attr : array()) - ); - } - - /** - * Returns an end tag - * @param string $tag Tag name - * @return string - */ - protected function end($tag) - { - return $this->generator->generateFromToken( - new HTMLPurifier_Token_End($tag) - ); - } - - /** - * Prints a complete element with content inside - * @param string $tag Tag name - * @param string $contents Element contents - * @param array $attr Tag attributes - * @param bool $escape whether or not to escape contents - * @return string - */ - protected function element($tag, $contents, $attr = array(), $escape = true) - { - return $this->start($tag, $attr) . - ($escape ? $this->escape($contents) : $contents) . - $this->end($tag); - } - - /** - * @param string $tag - * @param array $attr - * @return string - */ - protected function elementEmpty($tag, $attr = array()) - { - return $this->generator->generateFromToken( - new HTMLPurifier_Token_Empty($tag, $attr) - ); - } - - /** - * @param string $text - * @return string - */ - protected function text($text) - { - return $this->generator->generateFromToken( - new HTMLPurifier_Token_Text($text) - ); - } - - /** - * Prints a simple key/value row in a table. - * @param string $name Key - * @param mixed $value Value - * @return string - */ - protected function row($name, $value) - { - if (is_bool($value)) { - $value = $value ? 'On' : 'Off'; - } - return - $this->start('tr') . "\n" . - $this->element('th', $name) . "\n" . - $this->element('td', $value) . "\n" . - $this->end('tr'); - } - - /** - * Escapes a string for HTML output. - * @param string $string String to escape - * @return string - */ - protected function escape($string) - { - $string = HTMLPurifier_Encoder::cleanUTF8($string); - $string = htmlspecialchars($string, ENT_COMPAT, 'UTF-8'); - return $string; - } - - /** - * Takes a list of strings and turns them into a single list - * @param string[] $array List of strings - * @param bool $polite Bool whether or not to add an end before the last - * @return string - */ - protected function listify($array, $polite = false) - { - if (empty($array)) { - return 'None'; - } - $ret = ''; - $i = count($array); - foreach ($array as $value) { - $i--; - $ret .= $value; - if ($i > 0 && !($polite && $i == 1)) { - $ret .= ', '; - } - if ($polite && $i == 1) { - $ret .= 'and '; - } - } - return $ret; - } - - /** - * Retrieves the class of an object without prefixes, as well as metadata - * @param object $obj Object to determine class of - * @param string $sec_prefix Further prefix to remove - * @return string - */ - protected function getClass($obj, $sec_prefix = '') - { - static $five = null; - if ($five === null) { - $five = version_compare(PHP_VERSION, '5', '>='); - } - $prefix = 'HTMLPurifier_' . $sec_prefix; - if (!$five) { - $prefix = strtolower($prefix); - } - $class = str_replace($prefix, '', get_class($obj)); - $lclass = strtolower($class); - $class .= '('; - switch ($lclass) { - case 'enum': - $values = array(); - foreach ($obj->valid_values as $value => $bool) { - $values[] = $value; - } - $class .= implode(', ', $values); - break; - case 'css_composite': - $values = array(); - foreach ($obj->defs as $def) { - $values[] = $this->getClass($def, $sec_prefix); - } - $class .= implode(', ', $values); - break; - case 'css_multiple': - $class .= $this->getClass($obj->single, $sec_prefix) . ', '; - $class .= $obj->max; - break; - case 'css_denyelementdecorator': - $class .= $this->getClass($obj->def, $sec_prefix) . ', '; - $class .= $obj->element; - break; - case 'css_importantdecorator': - $class .= $this->getClass($obj->def, $sec_prefix); - if ($obj->allow) { - $class .= ', !important'; - } - break; - } - $class .= ')'; - return $class; - } -} - -// vim: et sw=4 sts=4 +getAll(); + $context = new HTMLPurifier_Context(); + $this->generator = new HTMLPurifier_Generator($config, $context); + } + + /** + * Main function that renders object or aspect of that object + * @note Parameters vary depending on printer + */ + // function render() {} + + /** + * Returns a start tag + * @param string $tag Tag name + * @param array $attr Attribute array + * @return string + */ + protected function start($tag, $attr = array()) + { + return $this->generator->generateFromToken( + new HTMLPurifier_Token_Start($tag, $attr ? $attr : array()) + ); + } + + /** + * Returns an end tag + * @param string $tag Tag name + * @return string + */ + protected function end($tag) + { + return $this->generator->generateFromToken( + new HTMLPurifier_Token_End($tag) + ); + } + + /** + * Prints a complete element with content inside + * @param string $tag Tag name + * @param string $contents Element contents + * @param array $attr Tag attributes + * @param bool $escape whether or not to escape contents + * @return string + */ + protected function element($tag, $contents, $attr = array(), $escape = true) + { + return $this->start($tag, $attr) . + ($escape ? $this->escape($contents) : $contents) . + $this->end($tag); + } + + /** + * @param string $tag + * @param array $attr + * @return string + */ + protected function elementEmpty($tag, $attr = array()) + { + return $this->generator->generateFromToken( + new HTMLPurifier_Token_Empty($tag, $attr) + ); + } + + /** + * @param string $text + * @return string + */ + protected function text($text) + { + return $this->generator->generateFromToken( + new HTMLPurifier_Token_Text($text) + ); + } + + /** + * Prints a simple key/value row in a table. + * @param string $name Key + * @param mixed $value Value + * @return string + */ + protected function row($name, $value) + { + if (is_bool($value)) { + $value = $value ? 'On' : 'Off'; + } + return + $this->start('tr') . "\n" . + $this->element('th', $name) . "\n" . + $this->element('td', $value) . "\n" . + $this->end('tr'); + } + + /** + * Escapes a string for HTML output. + * @param string $string String to escape + * @return string + */ + protected function escape($string) + { + $string = HTMLPurifier_Encoder::cleanUTF8($string); + $string = htmlspecialchars($string, ENT_COMPAT, 'UTF-8'); + return $string; + } + + /** + * Takes a list of strings and turns them into a single list + * @param string[] $array List of strings + * @param bool $polite Bool whether or not to add an end before the last + * @return string + */ + protected function listify($array, $polite = false) + { + if (empty($array)) { + return 'None'; + } + $ret = ''; + $i = count($array); + foreach ($array as $value) { + $i--; + $ret .= $value; + if ($i > 0 && !($polite && $i == 1)) { + $ret .= ', '; + } + if ($polite && $i == 1) { + $ret .= 'and '; + } + } + return $ret; + } + + /** + * Retrieves the class of an object without prefixes, as well as metadata + * @param object $obj Object to determine class of + * @param string $sec_prefix Further prefix to remove + * @return string + */ + protected function getClass($obj, $sec_prefix = '') + { + static $five = null; + if ($five === null) { + $five = version_compare(PHP_VERSION, '5', '>='); + } + $prefix = 'HTMLPurifier_' . $sec_prefix; + if (!$five) { + $prefix = strtolower($prefix); + } + $class = str_replace($prefix, '', get_class($obj)); + $lclass = strtolower($class); + $class .= '('; + switch ($lclass) { + case 'enum': + $values = array(); + foreach ($obj->valid_values as $value => $bool) { + $values[] = $value; + } + $class .= implode(', ', $values); + break; + case 'css_composite': + $values = array(); + foreach ($obj->defs as $def) { + $values[] = $this->getClass($def, $sec_prefix); + } + $class .= implode(', ', $values); + break; + case 'css_multiple': + $class .= $this->getClass($obj->single, $sec_prefix) . ', '; + $class .= $obj->max; + break; + case 'css_denyelementdecorator': + $class .= $this->getClass($obj->def, $sec_prefix) . ', '; + $class .= $obj->element; + break; + case 'css_importantdecorator': + $class .= $this->getClass($obj->def, $sec_prefix); + if ($obj->allow) { + $class .= ', !important'; + } + break; + } + $class .= ')'; + return $class; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/Printer/CSSDefinition.php b/lib/purifier/HTMLPurifier/Printer/CSSDefinition.php index afc8c18ab9..29505fe12d 100644 --- a/lib/purifier/HTMLPurifier/Printer/CSSDefinition.php +++ b/lib/purifier/HTMLPurifier/Printer/CSSDefinition.php @@ -1,44 +1,44 @@ -def = $config->getCSSDefinition(); - $ret = ''; - - $ret .= $this->start('div', array('class' => 'HTMLPurifier_Printer')); - $ret .= $this->start('table'); - - $ret .= $this->element('caption', 'Properties ($info)'); - - $ret .= $this->start('thead'); - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Property', array('class' => 'heavy')); - $ret .= $this->element('th', 'Definition', array('class' => 'heavy', 'style' => 'width:auto;')); - $ret .= $this->end('tr'); - $ret .= $this->end('thead'); - - ksort($this->def->info); - foreach ($this->def->info as $property => $obj) { - $name = $this->getClass($obj, 'AttrDef_'); - $ret .= $this->row($property, $name); - } - - $ret .= $this->end('table'); - $ret .= $this->end('div'); - - return $ret; - } -} - -// vim: et sw=4 sts=4 +def = $config->getCSSDefinition(); + $ret = ''; + + $ret .= $this->start('div', array('class' => 'HTMLPurifier_Printer')); + $ret .= $this->start('table'); + + $ret .= $this->element('caption', 'Properties ($info)'); + + $ret .= $this->start('thead'); + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Property', array('class' => 'heavy')); + $ret .= $this->element('th', 'Definition', array('class' => 'heavy', 'style' => 'width:auto;')); + $ret .= $this->end('tr'); + $ret .= $this->end('thead'); + + ksort($this->def->info); + foreach ($this->def->info as $property => $obj) { + $name = $this->getClass($obj, 'AttrDef_'); + $ret .= $this->row($property, $name); + } + + $ret .= $this->end('table'); + $ret .= $this->end('div'); + + return $ret; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/Printer/ConfigForm.css b/lib/purifier/HTMLPurifier/Printer/ConfigForm.css index 7af30fc3a0..3ff1a88aa4 100644 --- a/lib/purifier/HTMLPurifier/Printer/ConfigForm.css +++ b/lib/purifier/HTMLPurifier/Printer/ConfigForm.css @@ -1,10 +1,10 @@ - -.hp-config {} - -.hp-config tbody th {text-align:right; padding-right:0.5em;} -.hp-config thead, .hp-config .namespace {background:#3C578C; color:#FFF;} -.hp-config .namespace th {text-align:center;} -.hp-config .verbose {display:none;} -.hp-config .controls {text-align:center;} - -/* vim: et sw=4 sts=4 */ + +.hp-config {} + +.hp-config tbody th {text-align:right; padding-right:0.5em;} +.hp-config thead, .hp-config .namespace {background:#3C578C; color:#FFF;} +.hp-config .namespace th {text-align:center;} +.hp-config .verbose {display:none;} +.hp-config .controls {text-align:center;} + +/* vim: et sw=4 sts=4 */ diff --git a/lib/purifier/HTMLPurifier/Printer/ConfigForm.js b/lib/purifier/HTMLPurifier/Printer/ConfigForm.js index 83e0655314..cba00c9b80 100644 --- a/lib/purifier/HTMLPurifier/Printer/ConfigForm.js +++ b/lib/purifier/HTMLPurifier/Printer/ConfigForm.js @@ -1,5 +1,5 @@ -function toggleWriteability(id_of_patient, checked) { - document.getElementById(id_of_patient).disabled = checked; -} - -// vim: et sw=4 sts=4 +function toggleWriteability(id_of_patient, checked) { + document.getElementById(id_of_patient).disabled = checked; +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/Printer/ConfigForm.php b/lib/purifier/HTMLPurifier/Printer/ConfigForm.php index 3bc417366f..33ae11397e 100644 --- a/lib/purifier/HTMLPurifier/Printer/ConfigForm.php +++ b/lib/purifier/HTMLPurifier/Printer/ConfigForm.php @@ -1,451 +1,451 @@ -docURL = $doc_url; - $this->name = $name; - $this->compress = $compress; - // initialize sub-printers - $this->fields[0] = new HTMLPurifier_Printer_ConfigForm_default(); - $this->fields[HTMLPurifier_VarParser::BOOL] = new HTMLPurifier_Printer_ConfigForm_bool(); - } - - /** - * Sets default column and row size for textareas in sub-printers - * @param $cols Integer columns of textarea, null to use default - * @param $rows Integer rows of textarea, null to use default - */ - public function setTextareaDimensions($cols = null, $rows = null) - { - if ($cols) { - $this->fields['default']->cols = $cols; - } - if ($rows) { - $this->fields['default']->rows = $rows; - } - } - - /** - * Retrieves styling, in case it is not accessible by webserver - */ - public static function getCSS() - { - return file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/Printer/ConfigForm.css'); - } - - /** - * Retrieves JavaScript, in case it is not accessible by webserver - */ - public static function getJavaScript() - { - return file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/Printer/ConfigForm.js'); - } - - /** - * Returns HTML output for a configuration form - * @param HTMLPurifier_Config|array $config Configuration object of current form state, or an array - * where [0] has an HTML namespace and [1] is being rendered. - * @param array|bool $allowed Optional namespace(s) and directives to restrict form to. - * @param bool $render_controls - * @return string - */ - public function render($config, $allowed = true, $render_controls = true) - { - if (is_array($config) && isset($config[0])) { - $gen_config = $config[0]; - $config = $config[1]; - } else { - $gen_config = $config; - } - - $this->config = $config; - $this->genConfig = $gen_config; - $this->prepareGenerator($gen_config); - - $allowed = HTMLPurifier_Config::getAllowedDirectivesForForm($allowed, $config->def); - $all = array(); - foreach ($allowed as $key) { - list($ns, $directive) = $key; - $all[$ns][$directive] = $config->get($ns . '.' . $directive); - } - - $ret = ''; - $ret .= $this->start('table', array('class' => 'hp-config')); - $ret .= $this->start('thead'); - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Directive', array('class' => 'hp-directive')); - $ret .= $this->element('th', 'Value', array('class' => 'hp-value')); - $ret .= $this->end('tr'); - $ret .= $this->end('thead'); - foreach ($all as $ns => $directives) { - $ret .= $this->renderNamespace($ns, $directives); - } - if ($render_controls) { - $ret .= $this->start('tbody'); - $ret .= $this->start('tr'); - $ret .= $this->start('td', array('colspan' => 2, 'class' => 'controls')); - $ret .= $this->elementEmpty('input', array('type' => 'submit', 'value' => 'Submit')); - $ret .= '[Reset]'; - $ret .= $this->end('td'); - $ret .= $this->end('tr'); - $ret .= $this->end('tbody'); - } - $ret .= $this->end('table'); - return $ret; - } - - /** - * Renders a single namespace - * @param $ns String namespace name - * @param array $directives array of directives to values - * @return string - */ - protected function renderNamespace($ns, $directives) - { - $ret = ''; - $ret .= $this->start('tbody', array('class' => 'namespace')); - $ret .= $this->start('tr'); - $ret .= $this->element('th', $ns, array('colspan' => 2)); - $ret .= $this->end('tr'); - $ret .= $this->end('tbody'); - $ret .= $this->start('tbody'); - foreach ($directives as $directive => $value) { - $ret .= $this->start('tr'); - $ret .= $this->start('th'); - if ($this->docURL) { - $url = str_replace('%s', urlencode("$ns.$directive"), $this->docURL); - $ret .= $this->start('a', array('href' => $url)); - } - $attr = array('for' => "{$this->name}:$ns.$directive"); - - // crop directive name if it's too long - if (!$this->compress || (strlen($directive) < $this->compress)) { - $directive_disp = $directive; - } else { - $directive_disp = substr($directive, 0, $this->compress - 2) . '...'; - $attr['title'] = $directive; - } - - $ret .= $this->element( - 'label', - $directive_disp, - // component printers must create an element with this id - $attr - ); - if ($this->docURL) { - $ret .= $this->end('a'); - } - $ret .= $this->end('th'); - - $ret .= $this->start('td'); - $def = $this->config->def->info["$ns.$directive"]; - if (is_int($def)) { - $allow_null = $def < 0; - $type = abs($def); - } else { - $type = $def->type; - $allow_null = isset($def->allow_null); - } - if (!isset($this->fields[$type])) { - $type = 0; - } // default - $type_obj = $this->fields[$type]; - if ($allow_null) { - $type_obj = new HTMLPurifier_Printer_ConfigForm_NullDecorator($type_obj); - } - $ret .= $type_obj->render($ns, $directive, $value, $this->name, array($this->genConfig, $this->config)); - $ret .= $this->end('td'); - $ret .= $this->end('tr'); - } - $ret .= $this->end('tbody'); - return $ret; - } - -} - -/** - * Printer decorator for directives that accept null - */ -class HTMLPurifier_Printer_ConfigForm_NullDecorator extends HTMLPurifier_Printer -{ - /** - * Printer being decorated - * @type HTMLPurifier_Printer - */ - protected $obj; - - /** - * @param HTMLPurifier_Printer $obj Printer to decorate - */ - public function __construct($obj) - { - parent::__construct(); - $this->obj = $obj; - } - - /** - * @param string $ns - * @param string $directive - * @param string $value - * @param string $name - * @param HTMLPurifier_Config|array $config - * @return string - */ - public function render($ns, $directive, $value, $name, $config) - { - if (is_array($config) && isset($config[0])) { - $gen_config = $config[0]; - $config = $config[1]; - } else { - $gen_config = $config; - } - $this->prepareGenerator($gen_config); - - $ret = ''; - $ret .= $this->start('label', array('for' => "$name:Null_$ns.$directive")); - $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose')); - $ret .= $this->text(' Null/Disabled'); - $ret .= $this->end('label'); - $attr = array( - 'type' => 'checkbox', - 'value' => '1', - 'class' => 'null-toggle', - 'name' => "$name" . "[Null_$ns.$directive]", - 'id' => "$name:Null_$ns.$directive", - 'onclick' => "toggleWriteability('$name:$ns.$directive',checked)" // INLINE JAVASCRIPT!!!! - ); - if ($this->obj instanceof HTMLPurifier_Printer_ConfigForm_bool) { - // modify inline javascript slightly - $attr['onclick'] = - "toggleWriteability('$name:Yes_$ns.$directive',checked);" . - "toggleWriteability('$name:No_$ns.$directive',checked)"; - } - if ($value === null) { - $attr['checked'] = 'checked'; - } - $ret .= $this->elementEmpty('input', $attr); - $ret .= $this->text(' or '); - $ret .= $this->elementEmpty('br'); - $ret .= $this->obj->render($ns, $directive, $value, $name, array($gen_config, $config)); - return $ret; - } -} - -/** - * Swiss-army knife configuration form field printer - */ -class HTMLPurifier_Printer_ConfigForm_default extends HTMLPurifier_Printer -{ - /** - * @type int - */ - public $cols = 18; - - /** - * @type int - */ - public $rows = 5; - - /** - * @param string $ns - * @param string $directive - * @param string $value - * @param string $name - * @param HTMLPurifier_Config|array $config - * @return string - */ - public function render($ns, $directive, $value, $name, $config) - { - if (is_array($config) && isset($config[0])) { - $gen_config = $config[0]; - $config = $config[1]; - } else { - $gen_config = $config; - } - $this->prepareGenerator($gen_config); - // this should probably be split up a little - $ret = ''; - $def = $config->def->info["$ns.$directive"]; - if (is_int($def)) { - $type = abs($def); - } else { - $type = $def->type; - } - if (is_array($value)) { - switch ($type) { - case HTMLPurifier_VarParser::LOOKUP: - $array = $value; - $value = array(); - foreach ($array as $val => $b) { - $value[] = $val; - } - //TODO does this need a break? - case HTMLPurifier_VarParser::ALIST: - $value = implode(PHP_EOL, $value); - break; - case HTMLPurifier_VarParser::HASH: - $nvalue = ''; - foreach ($value as $i => $v) { - if (is_array($v)) { - // HACK - $v = implode(";", $v); - } - $nvalue .= "$i:$v" . PHP_EOL; - } - $value = $nvalue; - break; - default: - $value = ''; - } - } - if ($type === HTMLPurifier_VarParser::MIXED) { - return 'Not supported'; - $value = serialize($value); - } - $attr = array( - 'name' => "$name" . "[$ns.$directive]", - 'id' => "$name:$ns.$directive" - ); - if ($value === null) { - $attr['disabled'] = 'disabled'; - } - if (isset($def->allowed)) { - $ret .= $this->start('select', $attr); - foreach ($def->allowed as $val => $b) { - $attr = array(); - if ($value == $val) { - $attr['selected'] = 'selected'; - } - $ret .= $this->element('option', $val, $attr); - } - $ret .= $this->end('select'); - } elseif ($type === HTMLPurifier_VarParser::TEXT || - $type === HTMLPurifier_VarParser::ITEXT || - $type === HTMLPurifier_VarParser::ALIST || - $type === HTMLPurifier_VarParser::HASH || - $type === HTMLPurifier_VarParser::LOOKUP) { - $attr['cols'] = $this->cols; - $attr['rows'] = $this->rows; - $ret .= $this->start('textarea', $attr); - $ret .= $this->text($value); - $ret .= $this->end('textarea'); - } else { - $attr['value'] = $value; - $attr['type'] = 'text'; - $ret .= $this->elementEmpty('input', $attr); - } - return $ret; - } -} - -/** - * Bool form field printer - */ -class HTMLPurifier_Printer_ConfigForm_bool extends HTMLPurifier_Printer -{ - /** - * @param string $ns - * @param string $directive - * @param string $value - * @param string $name - * @param HTMLPurifier_Config|array $config - * @return string - */ - public function render($ns, $directive, $value, $name, $config) - { - if (is_array($config) && isset($config[0])) { - $gen_config = $config[0]; - $config = $config[1]; - } else { - $gen_config = $config; - } - $this->prepareGenerator($gen_config); - $ret = ''; - $ret .= $this->start('div', array('id' => "$name:$ns.$directive")); - - $ret .= $this->start('label', array('for' => "$name:Yes_$ns.$directive")); - $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose')); - $ret .= $this->text(' Yes'); - $ret .= $this->end('label'); - - $attr = array( - 'type' => 'radio', - 'name' => "$name" . "[$ns.$directive]", - 'id' => "$name:Yes_$ns.$directive", - 'value' => '1' - ); - if ($value === true) { - $attr['checked'] = 'checked'; - } - if ($value === null) { - $attr['disabled'] = 'disabled'; - } - $ret .= $this->elementEmpty('input', $attr); - - $ret .= $this->start('label', array('for' => "$name:No_$ns.$directive")); - $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose')); - $ret .= $this->text(' No'); - $ret .= $this->end('label'); - - $attr = array( - 'type' => 'radio', - 'name' => "$name" . "[$ns.$directive]", - 'id' => "$name:No_$ns.$directive", - 'value' => '0' - ); - if ($value === false) { - $attr['checked'] = 'checked'; - } - if ($value === null) { - $attr['disabled'] = 'disabled'; - } - $ret .= $this->elementEmpty('input', $attr); - - $ret .= $this->end('div'); - - return $ret; - } -} - -// vim: et sw=4 sts=4 +docURL = $doc_url; + $this->name = $name; + $this->compress = $compress; + // initialize sub-printers + $this->fields[0] = new HTMLPurifier_Printer_ConfigForm_default(); + $this->fields[HTMLPurifier_VarParser::C_BOOL] = new HTMLPurifier_Printer_ConfigForm_bool(); + } + + /** + * Sets default column and row size for textareas in sub-printers + * @param $cols Integer columns of textarea, null to use default + * @param $rows Integer rows of textarea, null to use default + */ + public function setTextareaDimensions($cols = null, $rows = null) + { + if ($cols) { + $this->fields['default']->cols = $cols; + } + if ($rows) { + $this->fields['default']->rows = $rows; + } + } + + /** + * Retrieves styling, in case it is not accessible by webserver + */ + public static function getCSS() + { + return file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/Printer/ConfigForm.css'); + } + + /** + * Retrieves JavaScript, in case it is not accessible by webserver + */ + public static function getJavaScript() + { + return file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/Printer/ConfigForm.js'); + } + + /** + * Returns HTML output for a configuration form + * @param HTMLPurifier_Config|array $config Configuration object of current form state, or an array + * where [0] has an HTML namespace and [1] is being rendered. + * @param array|bool $allowed Optional namespace(s) and directives to restrict form to. + * @param bool $render_controls + * @return string + */ + public function render($config, $allowed = true, $render_controls = true) + { + if (is_array($config) && isset($config[0])) { + $gen_config = $config[0]; + $config = $config[1]; + } else { + $gen_config = $config; + } + + $this->config = $config; + $this->genConfig = $gen_config; + $this->prepareGenerator($gen_config); + + $allowed = HTMLPurifier_Config::getAllowedDirectivesForForm($allowed, $config->def); + $all = array(); + foreach ($allowed as $key) { + list($ns, $directive) = $key; + $all[$ns][$directive] = $config->get($ns . '.' . $directive); + } + + $ret = ''; + $ret .= $this->start('table', array('class' => 'hp-config')); + $ret .= $this->start('thead'); + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Directive', array('class' => 'hp-directive')); + $ret .= $this->element('th', 'Value', array('class' => 'hp-value')); + $ret .= $this->end('tr'); + $ret .= $this->end('thead'); + foreach ($all as $ns => $directives) { + $ret .= $this->renderNamespace($ns, $directives); + } + if ($render_controls) { + $ret .= $this->start('tbody'); + $ret .= $this->start('tr'); + $ret .= $this->start('td', array('colspan' => 2, 'class' => 'controls')); + $ret .= $this->elementEmpty('input', array('type' => 'submit', 'value' => 'Submit')); + $ret .= '[Reset]'; + $ret .= $this->end('td'); + $ret .= $this->end('tr'); + $ret .= $this->end('tbody'); + } + $ret .= $this->end('table'); + return $ret; + } + + /** + * Renders a single namespace + * @param $ns String namespace name + * @param array $directives array of directives to values + * @return string + */ + protected function renderNamespace($ns, $directives) + { + $ret = ''; + $ret .= $this->start('tbody', array('class' => 'namespace')); + $ret .= $this->start('tr'); + $ret .= $this->element('th', $ns, array('colspan' => 2)); + $ret .= $this->end('tr'); + $ret .= $this->end('tbody'); + $ret .= $this->start('tbody'); + foreach ($directives as $directive => $value) { + $ret .= $this->start('tr'); + $ret .= $this->start('th'); + if ($this->docURL) { + $url = str_replace('%s', urlencode("$ns.$directive"), $this->docURL); + $ret .= $this->start('a', array('href' => $url)); + } + $attr = array('for' => "{$this->name}:$ns.$directive"); + + // crop directive name if it's too long + if (!$this->compress || (strlen($directive) < $this->compress)) { + $directive_disp = $directive; + } else { + $directive_disp = substr($directive, 0, $this->compress - 2) . '...'; + $attr['title'] = $directive; + } + + $ret .= $this->element( + 'label', + $directive_disp, + // component printers must create an element with this id + $attr + ); + if ($this->docURL) { + $ret .= $this->end('a'); + } + $ret .= $this->end('th'); + + $ret .= $this->start('td'); + $def = $this->config->def->info["$ns.$directive"]; + if (is_int($def)) { + $allow_null = $def < 0; + $type = abs($def); + } else { + $type = $def->type; + $allow_null = isset($def->allow_null); + } + if (!isset($this->fields[$type])) { + $type = 0; + } // default + $type_obj = $this->fields[$type]; + if ($allow_null) { + $type_obj = new HTMLPurifier_Printer_ConfigForm_NullDecorator($type_obj); + } + $ret .= $type_obj->render($ns, $directive, $value, $this->name, array($this->genConfig, $this->config)); + $ret .= $this->end('td'); + $ret .= $this->end('tr'); + } + $ret .= $this->end('tbody'); + return $ret; + } + +} + +/** + * Printer decorator for directives that accept null + */ +class HTMLPurifier_Printer_ConfigForm_NullDecorator extends HTMLPurifier_Printer +{ + /** + * Printer being decorated + * @type HTMLPurifier_Printer + */ + protected $obj; + + /** + * @param HTMLPurifier_Printer $obj Printer to decorate + */ + public function __construct($obj) + { + parent::__construct(); + $this->obj = $obj; + } + + /** + * @param string $ns + * @param string $directive + * @param string $value + * @param string $name + * @param HTMLPurifier_Config|array $config + * @return string + */ + public function render($ns, $directive, $value, $name, $config) + { + if (is_array($config) && isset($config[0])) { + $gen_config = $config[0]; + $config = $config[1]; + } else { + $gen_config = $config; + } + $this->prepareGenerator($gen_config); + + $ret = ''; + $ret .= $this->start('label', array('for' => "$name:Null_$ns.$directive")); + $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose')); + $ret .= $this->text(' Null/Disabled'); + $ret .= $this->end('label'); + $attr = array( + 'type' => 'checkbox', + 'value' => '1', + 'class' => 'null-toggle', + 'name' => "$name" . "[Null_$ns.$directive]", + 'id' => "$name:Null_$ns.$directive", + 'onclick' => "toggleWriteability('$name:$ns.$directive',checked)" // INLINE JAVASCRIPT!!!! + ); + if ($this->obj instanceof HTMLPurifier_Printer_ConfigForm_bool) { + // modify inline javascript slightly + $attr['onclick'] = + "toggleWriteability('$name:Yes_$ns.$directive',checked);" . + "toggleWriteability('$name:No_$ns.$directive',checked)"; + } + if ($value === null) { + $attr['checked'] = 'checked'; + } + $ret .= $this->elementEmpty('input', $attr); + $ret .= $this->text(' or '); + $ret .= $this->elementEmpty('br'); + $ret .= $this->obj->render($ns, $directive, $value, $name, array($gen_config, $config)); + return $ret; + } +} + +/** + * Swiss-army knife configuration form field printer + */ +class HTMLPurifier_Printer_ConfigForm_default extends HTMLPurifier_Printer +{ + /** + * @type int + */ + public $cols = 18; + + /** + * @type int + */ + public $rows = 5; + + /** + * @param string $ns + * @param string $directive + * @param string $value + * @param string $name + * @param HTMLPurifier_Config|array $config + * @return string + */ + public function render($ns, $directive, $value, $name, $config) + { + if (is_array($config) && isset($config[0])) { + $gen_config = $config[0]; + $config = $config[1]; + } else { + $gen_config = $config; + } + $this->prepareGenerator($gen_config); + // this should probably be split up a little + $ret = ''; + $def = $config->def->info["$ns.$directive"]; + if (is_int($def)) { + $type = abs($def); + } else { + $type = $def->type; + } + if (is_array($value)) { + switch ($type) { + case HTMLPurifier_VarParser::LOOKUP: + $array = $value; + $value = array(); + foreach ($array as $val => $b) { + $value[] = $val; + } + //TODO does this need a break? + case HTMLPurifier_VarParser::ALIST: + $value = implode(PHP_EOL, $value); + break; + case HTMLPurifier_VarParser::HASH: + $nvalue = ''; + foreach ($value as $i => $v) { + if (is_array($v)) { + // HACK + $v = implode(";", $v); + } + $nvalue .= "$i:$v" . PHP_EOL; + } + $value = $nvalue; + break; + default: + $value = ''; + } + } + if ($type === HTMLPurifier_VarParser::C_MIXED) { + return 'Not supported'; + $value = serialize($value); + } + $attr = array( + 'name' => "$name" . "[$ns.$directive]", + 'id' => "$name:$ns.$directive" + ); + if ($value === null) { + $attr['disabled'] = 'disabled'; + } + if (isset($def->allowed)) { + $ret .= $this->start('select', $attr); + foreach ($def->allowed as $val => $b) { + $attr = array(); + if ($value == $val) { + $attr['selected'] = 'selected'; + } + $ret .= $this->element('option', $val, $attr); + } + $ret .= $this->end('select'); + } elseif ($type === HTMLPurifier_VarParser::TEXT || + $type === HTMLPurifier_VarParser::ITEXT || + $type === HTMLPurifier_VarParser::ALIST || + $type === HTMLPurifier_VarParser::HASH || + $type === HTMLPurifier_VarParser::LOOKUP) { + $attr['cols'] = $this->cols; + $attr['rows'] = $this->rows; + $ret .= $this->start('textarea', $attr); + $ret .= $this->text($value); + $ret .= $this->end('textarea'); + } else { + $attr['value'] = $value; + $attr['type'] = 'text'; + $ret .= $this->elementEmpty('input', $attr); + } + return $ret; + } +} + +/** + * Bool form field printer + */ +class HTMLPurifier_Printer_ConfigForm_bool extends HTMLPurifier_Printer +{ + /** + * @param string $ns + * @param string $directive + * @param string $value + * @param string $name + * @param HTMLPurifier_Config|array $config + * @return string + */ + public function render($ns, $directive, $value, $name, $config) + { + if (is_array($config) && isset($config[0])) { + $gen_config = $config[0]; + $config = $config[1]; + } else { + $gen_config = $config; + } + $this->prepareGenerator($gen_config); + $ret = ''; + $ret .= $this->start('div', array('id' => "$name:$ns.$directive")); + + $ret .= $this->start('label', array('for' => "$name:Yes_$ns.$directive")); + $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose')); + $ret .= $this->text(' Yes'); + $ret .= $this->end('label'); + + $attr = array( + 'type' => 'radio', + 'name' => "$name" . "[$ns.$directive]", + 'id' => "$name:Yes_$ns.$directive", + 'value' => '1' + ); + if ($value === true) { + $attr['checked'] = 'checked'; + } + if ($value === null) { + $attr['disabled'] = 'disabled'; + } + $ret .= $this->elementEmpty('input', $attr); + + $ret .= $this->start('label', array('for' => "$name:No_$ns.$directive")); + $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose')); + $ret .= $this->text(' No'); + $ret .= $this->end('label'); + + $attr = array( + 'type' => 'radio', + 'name' => "$name" . "[$ns.$directive]", + 'id' => "$name:No_$ns.$directive", + 'value' => '0' + ); + if ($value === false) { + $attr['checked'] = 'checked'; + } + if ($value === null) { + $attr['disabled'] = 'disabled'; + } + $ret .= $this->elementEmpty('input', $attr); + + $ret .= $this->end('div'); + + return $ret; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/Printer/HTMLDefinition.php b/lib/purifier/HTMLPurifier/Printer/HTMLDefinition.php index 679d19ba3a..ae8639176e 100644 --- a/lib/purifier/HTMLPurifier/Printer/HTMLDefinition.php +++ b/lib/purifier/HTMLPurifier/Printer/HTMLDefinition.php @@ -1,324 +1,324 @@ -config =& $config; - - $this->def = $config->getHTMLDefinition(); - - $ret .= $this->start('div', array('class' => 'HTMLPurifier_Printer')); - - $ret .= $this->renderDoctype(); - $ret .= $this->renderEnvironment(); - $ret .= $this->renderContentSets(); - $ret .= $this->renderInfo(); - - $ret .= $this->end('div'); - - return $ret; - } - - /** - * Renders the Doctype table - * @return string - */ - protected function renderDoctype() - { - $doctype = $this->def->doctype; - $ret = ''; - $ret .= $this->start('table'); - $ret .= $this->element('caption', 'Doctype'); - $ret .= $this->row('Name', $doctype->name); - $ret .= $this->row('XML', $doctype->xml ? 'Yes' : 'No'); - $ret .= $this->row('Default Modules', implode($doctype->modules, ', ')); - $ret .= $this->row('Default Tidy Modules', implode($doctype->tidyModules, ', ')); - $ret .= $this->end('table'); - return $ret; - } - - - /** - * Renders environment table, which is miscellaneous info - * @return string - */ - protected function renderEnvironment() - { - $def = $this->def; - - $ret = ''; - - $ret .= $this->start('table'); - $ret .= $this->element('caption', 'Environment'); - - $ret .= $this->row('Parent of fragment', $def->info_parent); - $ret .= $this->renderChildren($def->info_parent_def->child); - $ret .= $this->row('Block wrap name', $def->info_block_wrapper); - - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Global attributes'); - $ret .= $this->element('td', $this->listifyAttr($def->info_global_attr), null, 0); - $ret .= $this->end('tr'); - - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Tag transforms'); - $list = array(); - foreach ($def->info_tag_transform as $old => $new) { - $new = $this->getClass($new, 'TagTransform_'); - $list[] = "<$old> with $new"; - } - $ret .= $this->element('td', $this->listify($list)); - $ret .= $this->end('tr'); - - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Pre-AttrTransform'); - $ret .= $this->element('td', $this->listifyObjectList($def->info_attr_transform_pre)); - $ret .= $this->end('tr'); - - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Post-AttrTransform'); - $ret .= $this->element('td', $this->listifyObjectList($def->info_attr_transform_post)); - $ret .= $this->end('tr'); - - $ret .= $this->end('table'); - return $ret; - } - - /** - * Renders the Content Sets table - * @return string - */ - protected function renderContentSets() - { - $ret = ''; - $ret .= $this->start('table'); - $ret .= $this->element('caption', 'Content Sets'); - foreach ($this->def->info_content_sets as $name => $lookup) { - $ret .= $this->heavyHeader($name); - $ret .= $this->start('tr'); - $ret .= $this->element('td', $this->listifyTagLookup($lookup)); - $ret .= $this->end('tr'); - } - $ret .= $this->end('table'); - return $ret; - } - - /** - * Renders the Elements ($info) table - * @return string - */ - protected function renderInfo() - { - $ret = ''; - $ret .= $this->start('table'); - $ret .= $this->element('caption', 'Elements ($info)'); - ksort($this->def->info); - $ret .= $this->heavyHeader('Allowed tags', 2); - $ret .= $this->start('tr'); - $ret .= $this->element('td', $this->listifyTagLookup($this->def->info), array('colspan' => 2)); - $ret .= $this->end('tr'); - foreach ($this->def->info as $name => $def) { - $ret .= $this->start('tr'); - $ret .= $this->element('th', "<$name>", array('class' => 'heavy', 'colspan' => 2)); - $ret .= $this->end('tr'); - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Inline content'); - $ret .= $this->element('td', $def->descendants_are_inline ? 'Yes' : 'No'); - $ret .= $this->end('tr'); - if (!empty($def->excludes)) { - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Excludes'); - $ret .= $this->element('td', $this->listifyTagLookup($def->excludes)); - $ret .= $this->end('tr'); - } - if (!empty($def->attr_transform_pre)) { - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Pre-AttrTransform'); - $ret .= $this->element('td', $this->listifyObjectList($def->attr_transform_pre)); - $ret .= $this->end('tr'); - } - if (!empty($def->attr_transform_post)) { - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Post-AttrTransform'); - $ret .= $this->element('td', $this->listifyObjectList($def->attr_transform_post)); - $ret .= $this->end('tr'); - } - if (!empty($def->auto_close)) { - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Auto closed by'); - $ret .= $this->element('td', $this->listifyTagLookup($def->auto_close)); - $ret .= $this->end('tr'); - } - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Allowed attributes'); - $ret .= $this->element('td', $this->listifyAttr($def->attr), array(), 0); - $ret .= $this->end('tr'); - - if (!empty($def->required_attr)) { - $ret .= $this->row('Required attributes', $this->listify($def->required_attr)); - } - - $ret .= $this->renderChildren($def->child); - } - $ret .= $this->end('table'); - return $ret; - } - - /** - * Renders a row describing the allowed children of an element - * @param HTMLPurifier_ChildDef $def HTMLPurifier_ChildDef of pertinent element - * @return string - */ - protected function renderChildren($def) - { - $context = new HTMLPurifier_Context(); - $ret = ''; - $ret .= $this->start('tr'); - $elements = array(); - $attr = array(); - if (isset($def->elements)) { - if ($def->type == 'strictblockquote') { - $def->validateChildren(array(), $this->config, $context); - } - $elements = $def->elements; - } - if ($def->type == 'chameleon') { - $attr['rowspan'] = 2; - } elseif ($def->type == 'empty') { - $elements = array(); - } elseif ($def->type == 'table') { - $elements = array_flip( - array( - 'col', - 'caption', - 'colgroup', - 'thead', - 'tfoot', - 'tbody', - 'tr' - ) - ); - } - $ret .= $this->element('th', 'Allowed children', $attr); - - if ($def->type == 'chameleon') { - - $ret .= $this->element( - 'td', - 'Block: ' . - $this->escape($this->listifyTagLookup($def->block->elements)), - null, - 0 - ); - $ret .= $this->end('tr'); - $ret .= $this->start('tr'); - $ret .= $this->element( - 'td', - 'Inline: ' . - $this->escape($this->listifyTagLookup($def->inline->elements)), - null, - 0 - ); - - } elseif ($def->type == 'custom') { - - $ret .= $this->element( - 'td', - '' . ucfirst($def->type) . ': ' . - $def->dtd_regex - ); - - } else { - $ret .= $this->element( - 'td', - '' . ucfirst($def->type) . ': ' . - $this->escape($this->listifyTagLookup($elements)), - null, - 0 - ); - } - $ret .= $this->end('tr'); - return $ret; - } - - /** - * Listifies a tag lookup table. - * @param array $array Tag lookup array in form of array('tagname' => true) - * @return string - */ - protected function listifyTagLookup($array) - { - ksort($array); - $list = array(); - foreach ($array as $name => $discard) { - if ($name !== '#PCDATA' && !isset($this->def->info[$name])) { - continue; - } - $list[] = $name; - } - return $this->listify($list); - } - - /** - * Listifies a list of objects by retrieving class names and internal state - * @param array $array List of objects - * @return string - * @todo Also add information about internal state - */ - protected function listifyObjectList($array) - { - ksort($array); - $list = array(); - foreach ($array as $obj) { - $list[] = $this->getClass($obj, 'AttrTransform_'); - } - return $this->listify($list); - } - - /** - * Listifies a hash of attributes to AttrDef classes - * @param array $array Array hash in form of array('attrname' => HTMLPurifier_AttrDef) - * @return string - */ - protected function listifyAttr($array) - { - ksort($array); - $list = array(); - foreach ($array as $name => $obj) { - if ($obj === false) { - continue; - } - $list[] = "$name = " . $this->getClass($obj, 'AttrDef_') . ''; - } - return $this->listify($list); - } - - /** - * Creates a heavy header row - * @param string $text - * @param int $num - * @return string - */ - protected function heavyHeader($text, $num = 1) - { - $ret = ''; - $ret .= $this->start('tr'); - $ret .= $this->element('th', $text, array('colspan' => $num, 'class' => 'heavy')); - $ret .= $this->end('tr'); - return $ret; - } -} - -// vim: et sw=4 sts=4 +config =& $config; + + $this->def = $config->getHTMLDefinition(); + + $ret .= $this->start('div', array('class' => 'HTMLPurifier_Printer')); + + $ret .= $this->renderDoctype(); + $ret .= $this->renderEnvironment(); + $ret .= $this->renderContentSets(); + $ret .= $this->renderInfo(); + + $ret .= $this->end('div'); + + return $ret; + } + + /** + * Renders the Doctype table + * @return string + */ + protected function renderDoctype() + { + $doctype = $this->def->doctype; + $ret = ''; + $ret .= $this->start('table'); + $ret .= $this->element('caption', 'Doctype'); + $ret .= $this->row('Name', $doctype->name); + $ret .= $this->row('XML', $doctype->xml ? 'Yes' : 'No'); + $ret .= $this->row('Default Modules', implode(', ', $doctype->modules)); + $ret .= $this->row('Default Tidy Modules', implode(', ', $doctype->tidyModules)); + $ret .= $this->end('table'); + return $ret; + } + + + /** + * Renders environment table, which is miscellaneous info + * @return string + */ + protected function renderEnvironment() + { + $def = $this->def; + + $ret = ''; + + $ret .= $this->start('table'); + $ret .= $this->element('caption', 'Environment'); + + $ret .= $this->row('Parent of fragment', $def->info_parent); + $ret .= $this->renderChildren($def->info_parent_def->child); + $ret .= $this->row('Block wrap name', $def->info_block_wrapper); + + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Global attributes'); + $ret .= $this->element('td', $this->listifyAttr($def->info_global_attr), null, 0); + $ret .= $this->end('tr'); + + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Tag transforms'); + $list = array(); + foreach ($def->info_tag_transform as $old => $new) { + $new = $this->getClass($new, 'TagTransform_'); + $list[] = "<$old> with $new"; + } + $ret .= $this->element('td', $this->listify($list)); + $ret .= $this->end('tr'); + + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Pre-AttrTransform'); + $ret .= $this->element('td', $this->listifyObjectList($def->info_attr_transform_pre)); + $ret .= $this->end('tr'); + + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Post-AttrTransform'); + $ret .= $this->element('td', $this->listifyObjectList($def->info_attr_transform_post)); + $ret .= $this->end('tr'); + + $ret .= $this->end('table'); + return $ret; + } + + /** + * Renders the Content Sets table + * @return string + */ + protected function renderContentSets() + { + $ret = ''; + $ret .= $this->start('table'); + $ret .= $this->element('caption', 'Content Sets'); + foreach ($this->def->info_content_sets as $name => $lookup) { + $ret .= $this->heavyHeader($name); + $ret .= $this->start('tr'); + $ret .= $this->element('td', $this->listifyTagLookup($lookup)); + $ret .= $this->end('tr'); + } + $ret .= $this->end('table'); + return $ret; + } + + /** + * Renders the Elements ($info) table + * @return string + */ + protected function renderInfo() + { + $ret = ''; + $ret .= $this->start('table'); + $ret .= $this->element('caption', 'Elements ($info)'); + ksort($this->def->info); + $ret .= $this->heavyHeader('Allowed tags', 2); + $ret .= $this->start('tr'); + $ret .= $this->element('td', $this->listifyTagLookup($this->def->info), array('colspan' => 2)); + $ret .= $this->end('tr'); + foreach ($this->def->info as $name => $def) { + $ret .= $this->start('tr'); + $ret .= $this->element('th', "<$name>", array('class' => 'heavy', 'colspan' => 2)); + $ret .= $this->end('tr'); + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Inline content'); + $ret .= $this->element('td', $def->descendants_are_inline ? 'Yes' : 'No'); + $ret .= $this->end('tr'); + if (!empty($def->excludes)) { + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Excludes'); + $ret .= $this->element('td', $this->listifyTagLookup($def->excludes)); + $ret .= $this->end('tr'); + } + if (!empty($def->attr_transform_pre)) { + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Pre-AttrTransform'); + $ret .= $this->element('td', $this->listifyObjectList($def->attr_transform_pre)); + $ret .= $this->end('tr'); + } + if (!empty($def->attr_transform_post)) { + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Post-AttrTransform'); + $ret .= $this->element('td', $this->listifyObjectList($def->attr_transform_post)); + $ret .= $this->end('tr'); + } + if (!empty($def->auto_close)) { + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Auto closed by'); + $ret .= $this->element('td', $this->listifyTagLookup($def->auto_close)); + $ret .= $this->end('tr'); + } + $ret .= $this->start('tr'); + $ret .= $this->element('th', 'Allowed attributes'); + $ret .= $this->element('td', $this->listifyAttr($def->attr), array(), 0); + $ret .= $this->end('tr'); + + if (!empty($def->required_attr)) { + $ret .= $this->row('Required attributes', $this->listify($def->required_attr)); + } + + $ret .= $this->renderChildren($def->child); + } + $ret .= $this->end('table'); + return $ret; + } + + /** + * Renders a row describing the allowed children of an element + * @param HTMLPurifier_ChildDef $def HTMLPurifier_ChildDef of pertinent element + * @return string + */ + protected function renderChildren($def) + { + $context = new HTMLPurifier_Context(); + $ret = ''; + $ret .= $this->start('tr'); + $elements = array(); + $attr = array(); + if (isset($def->elements)) { + if ($def->type == 'strictblockquote') { + $def->validateChildren(array(), $this->config, $context); + } + $elements = $def->elements; + } + if ($def->type == 'chameleon') { + $attr['rowspan'] = 2; + } elseif ($def->type == 'empty') { + $elements = array(); + } elseif ($def->type == 'table') { + $elements = array_flip( + array( + 'col', + 'caption', + 'colgroup', + 'thead', + 'tfoot', + 'tbody', + 'tr' + ) + ); + } + $ret .= $this->element('th', 'Allowed children', $attr); + + if ($def->type == 'chameleon') { + + $ret .= $this->element( + 'td', + 'Block: ' . + $this->escape($this->listifyTagLookup($def->block->elements)), + null, + 0 + ); + $ret .= $this->end('tr'); + $ret .= $this->start('tr'); + $ret .= $this->element( + 'td', + 'Inline: ' . + $this->escape($this->listifyTagLookup($def->inline->elements)), + null, + 0 + ); + + } elseif ($def->type == 'custom') { + + $ret .= $this->element( + 'td', + '' . ucfirst($def->type) . ': ' . + $def->dtd_regex + ); + + } else { + $ret .= $this->element( + 'td', + '' . ucfirst($def->type) . ': ' . + $this->escape($this->listifyTagLookup($elements)), + null, + 0 + ); + } + $ret .= $this->end('tr'); + return $ret; + } + + /** + * Listifies a tag lookup table. + * @param array $array Tag lookup array in form of array('tagname' => true) + * @return string + */ + protected function listifyTagLookup($array) + { + ksort($array); + $list = array(); + foreach ($array as $name => $discard) { + if ($name !== '#PCDATA' && !isset($this->def->info[$name])) { + continue; + } + $list[] = $name; + } + return $this->listify($list); + } + + /** + * Listifies a list of objects by retrieving class names and internal state + * @param array $array List of objects + * @return string + * @todo Also add information about internal state + */ + protected function listifyObjectList($array) + { + ksort($array); + $list = array(); + foreach ($array as $obj) { + $list[] = $this->getClass($obj, 'AttrTransform_'); + } + return $this->listify($list); + } + + /** + * Listifies a hash of attributes to AttrDef classes + * @param array $array Array hash in form of array('attrname' => HTMLPurifier_AttrDef) + * @return string + */ + protected function listifyAttr($array) + { + ksort($array); + $list = array(); + foreach ($array as $name => $obj) { + if ($obj === false) { + continue; + } + $list[] = "$name = " . $this->getClass($obj, 'AttrDef_') . ''; + } + return $this->listify($list); + } + + /** + * Creates a heavy header row + * @param string $text + * @param int $num + * @return string + */ + protected function heavyHeader($text, $num = 1) + { + $ret = ''; + $ret .= $this->start('tr'); + $ret .= $this->element('th', $text, array('colspan' => $num, 'class' => 'heavy')); + $ret .= $this->end('tr'); + return $ret; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/PropertyList.php b/lib/purifier/HTMLPurifier/PropertyList.php index d27fd53ec1..189348fd9e 100644 --- a/lib/purifier/HTMLPurifier/PropertyList.php +++ b/lib/purifier/HTMLPurifier/PropertyList.php @@ -1,122 +1,122 @@ -parent = $parent; - } - - /** - * Recursively retrieves the value for a key - * @param string $name - * @throws HTMLPurifier_Exception - */ - public function get($name) - { - if ($this->has($name)) { - return $this->data[$name]; - } - // possible performance bottleneck, convert to iterative if necessary - if ($this->parent) { - return $this->parent->get($name); - } - throw new HTMLPurifier_Exception("Key '$name' not found"); - } - - /** - * Sets the value of a key, for this plist - * @param string $name - * @param mixed $value - */ - public function set($name, $value) - { - $this->data[$name] = $value; - } - - /** - * Returns true if a given key exists - * @param string $name - * @return bool - */ - public function has($name) - { - return array_key_exists($name, $this->data); - } - - /** - * Resets a value to the value of it's parent, usually the default. If - * no value is specified, the entire plist is reset. - * @param string $name - */ - public function reset($name = null) - { - if ($name == null) { - $this->data = array(); - } else { - unset($this->data[$name]); - } - } - - /** - * Squashes this property list and all of its property lists into a single - * array, and returns the array. This value is cached by default. - * @param bool $force If true, ignores the cache and regenerates the array. - * @return array - */ - public function squash($force = false) - { - if ($this->cache !== null && !$force) { - return $this->cache; - } - if ($this->parent) { - return $this->cache = array_merge($this->parent->squash($force), $this->data); - } else { - return $this->cache = $this->data; - } - } - - /** - * Returns the parent plist. - * @return HTMLPurifier_PropertyList - */ - public function getParent() - { - return $this->parent; - } - - /** - * Sets the parent plist. - * @param HTMLPurifier_PropertyList $plist Parent plist - */ - public function setParent($plist) - { - $this->parent = $plist; - } -} - -// vim: et sw=4 sts=4 +parent = $parent; + } + + /** + * Recursively retrieves the value for a key + * @param string $name + * @throws HTMLPurifier_Exception + */ + public function get($name) + { + if ($this->has($name)) { + return $this->data[$name]; + } + // possible performance bottleneck, convert to iterative if necessary + if ($this->parent) { + return $this->parent->get($name); + } + throw new HTMLPurifier_Exception("Key '$name' not found"); + } + + /** + * Sets the value of a key, for this plist + * @param string $name + * @param mixed $value + */ + public function set($name, $value) + { + $this->data[$name] = $value; + } + + /** + * Returns true if a given key exists + * @param string $name + * @return bool + */ + public function has($name) + { + return array_key_exists($name, $this->data); + } + + /** + * Resets a value to the value of it's parent, usually the default. If + * no value is specified, the entire plist is reset. + * @param string $name + */ + public function reset($name = null) + { + if ($name == null) { + $this->data = array(); + } else { + unset($this->data[$name]); + } + } + + /** + * Squashes this property list and all of its property lists into a single + * array, and returns the array. This value is cached by default. + * @param bool $force If true, ignores the cache and regenerates the array. + * @return array + */ + public function squash($force = false) + { + if ($this->cache !== null && !$force) { + return $this->cache; + } + if ($this->parent) { + return $this->cache = array_merge($this->parent->squash($force), $this->data); + } else { + return $this->cache = $this->data; + } + } + + /** + * Returns the parent plist. + * @return HTMLPurifier_PropertyList + */ + public function getParent() + { + return $this->parent; + } + + /** + * Sets the parent plist. + * @param HTMLPurifier_PropertyList $plist Parent plist + */ + public function setParent($plist) + { + $this->parent = $plist; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/PropertyListIterator.php b/lib/purifier/HTMLPurifier/PropertyListIterator.php index 1e707e2ae3..15b330ea30 100644 --- a/lib/purifier/HTMLPurifier/PropertyListIterator.php +++ b/lib/purifier/HTMLPurifier/PropertyListIterator.php @@ -1,42 +1,42 @@ -l = strlen($filter); - $this->filter = $filter; - } - - /** - * @return bool - */ - public function accept() - { - $key = $this->getInnerIterator()->key(); - if (strncmp($key, $this->filter, $this->l) !== 0) { - return false; - } - return true; - } -} - -// vim: et sw=4 sts=4 +l = strlen($filter); + $this->filter = $filter; + } + + /** + * @return bool + */ + public function accept() + { + $key = $this->getInnerIterator()->key(); + if (strncmp($key, $this->filter, $this->l) !== 0) { + return false; + } + return true; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/Queue.php b/lib/purifier/HTMLPurifier/Queue.php index a75894d451..f58db9042a 100644 --- a/lib/purifier/HTMLPurifier/Queue.php +++ b/lib/purifier/HTMLPurifier/Queue.php @@ -1,56 +1,56 @@ -input = $input; - $this->output = array(); - } - - /** - * Shifts an element off the front of the queue. - */ - public function shift() { - if (empty($this->output)) { - $this->output = array_reverse($this->input); - $this->input = array(); - } - if (empty($this->output)) { - return NULL; - } - return array_pop($this->output); - } - - /** - * Pushes an element onto the front of the queue. - */ - public function push($x) { - array_push($this->input, $x); - } - - /** - * Checks if it's empty. - */ - public function isEmpty() { - return empty($this->input) && empty($this->output); - } -} +input = $input; + $this->output = array(); + } + + /** + * Shifts an element off the front of the queue. + */ + public function shift() { + if (empty($this->output)) { + $this->output = array_reverse($this->input); + $this->input = array(); + } + if (empty($this->output)) { + return NULL; + } + return array_pop($this->output); + } + + /** + * Pushes an element onto the front of the queue. + */ + public function push($x) { + array_push($this->input, $x); + } + + /** + * Checks if it's empty. + */ + public function isEmpty() { + return empty($this->input) && empty($this->output); + } +} diff --git a/lib/purifier/HTMLPurifier/Strategy.php b/lib/purifier/HTMLPurifier/Strategy.php index 291eb83c93..e1ff3b72df 100644 --- a/lib/purifier/HTMLPurifier/Strategy.php +++ b/lib/purifier/HTMLPurifier/Strategy.php @@ -1,26 +1,26 @@ -strategies as $strategy) { - $tokens = $strategy->execute($tokens, $config, $context); - } - return $tokens; - } -} - -// vim: et sw=4 sts=4 +strategies as $strategy) { + $tokens = $strategy->execute($tokens, $config, $context); + } + return $tokens; + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/Strategy/Core.php b/lib/purifier/HTMLPurifier/Strategy/Core.php index 077521523c..4414c17d6e 100644 --- a/lib/purifier/HTMLPurifier/Strategy/Core.php +++ b/lib/purifier/HTMLPurifier/Strategy/Core.php @@ -1,17 +1,17 @@ -strategies[] = new HTMLPurifier_Strategy_RemoveForeignElements(); - $this->strategies[] = new HTMLPurifier_Strategy_MakeWellFormed(); - $this->strategies[] = new HTMLPurifier_Strategy_FixNesting(); - $this->strategies[] = new HTMLPurifier_Strategy_ValidateAttributes(); - } -} - -// vim: et sw=4 sts=4 +strategies[] = new HTMLPurifier_Strategy_RemoveForeignElements(); + $this->strategies[] = new HTMLPurifier_Strategy_MakeWellFormed(); + $this->strategies[] = new HTMLPurifier_Strategy_FixNesting(); + $this->strategies[] = new HTMLPurifier_Strategy_ValidateAttributes(); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/Strategy/FixNesting.php b/lib/purifier/HTMLPurifier/Strategy/FixNesting.php index 8b1eb20fb0..6fa673db9a 100644 --- a/lib/purifier/HTMLPurifier/Strategy/FixNesting.php +++ b/lib/purifier/HTMLPurifier/Strategy/FixNesting.php @@ -1,181 +1,181 @@ -getHTMLDefinition(); - - $excludes_enabled = !$config->get('Core.DisableExcludes'); - - // setup the context variable 'IsInline', for chameleon processing - // is 'false' when we are not inline, 'true' when it must always - // be inline, and an integer when it is inline for a certain - // branch of the document tree - $is_inline = $definition->info_parent_def->descendants_are_inline; - $context->register('IsInline', $is_inline); - - // setup error collector - $e =& $context->get('ErrorCollector', true); - - //####################################################################// - // Loop initialization - - // stack that contains all elements that are excluded - // it is organized by parent elements, similar to $stack, - // but it is only populated when an element with exclusions is - // processed, i.e. there won't be empty exclusions. - $exclude_stack = array($definition->info_parent_def->excludes); - - // variable that contains the start token while we are processing - // nodes. This enables error reporting to do its job - $node = $top_node; - // dummy token - list($token, $d) = $node->toTokenPair(); - $context->register('CurrentNode', $node); - $context->register('CurrentToken', $token); - - //####################################################################// - // Loop - - // We need to implement a post-order traversal iteratively, to - // avoid running into stack space limits. This is pretty tricky - // to reason about, so we just manually stack-ify the recursive - // variant: - // - // function f($node) { - // foreach ($node->children as $child) { - // f($child); - // } - // validate($node); - // } - // - // Thus, we will represent a stack frame as array($node, - // $is_inline, stack of children) - // e.g. array_reverse($node->children) - already processed - // children. - - $parent_def = $definition->info_parent_def; - $stack = array( - array($top_node, - $parent_def->descendants_are_inline, - $parent_def->excludes, // exclusions - 0) - ); - - while (!empty($stack)) { - list($node, $is_inline, $excludes, $ix) = array_pop($stack); - // recursive call - $go = false; - $def = empty($stack) ? $definition->info_parent_def : $definition->info[$node->name]; - while (isset($node->children[$ix])) { - $child = $node->children[$ix++]; - if ($child instanceof HTMLPurifier_Node_Element) { - $go = true; - $stack[] = array($node, $is_inline, $excludes, $ix); - $stack[] = array($child, - // ToDo: I don't think it matters if it's def or - // child_def, but double check this... - $is_inline || $def->descendants_are_inline, - empty($def->excludes) ? $excludes - : array_merge($excludes, $def->excludes), - 0); - break; - } - }; - if ($go) continue; - list($token, $d) = $node->toTokenPair(); - // base case - if ($excludes_enabled && isset($excludes[$node->name])) { - $node->dead = true; - if ($e) $e->send(E_ERROR, 'Strategy_FixNesting: Node excluded'); - } else { - // XXX I suppose it would be slightly more efficient to - // avoid the allocation here and have children - // strategies handle it - $children = array(); - foreach ($node->children as $child) { - if (!$child->dead) $children[] = $child; - } - $result = $def->child->validateChildren($children, $config, $context); - if ($result === true) { - // nop - $node->children = $children; - } elseif ($result === false) { - $node->dead = true; - if ($e) $e->send(E_ERROR, 'Strategy_FixNesting: Node removed'); - } else { - $node->children = $result; - if ($e) { - // XXX This will miss mutations of internal nodes. Perhaps defer to the child validators - if (empty($result) && !empty($children)) { - $e->send(E_ERROR, 'Strategy_FixNesting: Node contents removed'); - } else if ($result != $children) { - $e->send(E_WARNING, 'Strategy_FixNesting: Node reorganized'); - } - } - } - } - } - - //####################################################################// - // Post-processing - - // remove context variables - $context->destroy('IsInline'); - $context->destroy('CurrentNode'); - $context->destroy('CurrentToken'); - - //####################################################################// - // Return - - return HTMLPurifier_Arborize::flatten($node, $config, $context); - } -} - -// vim: et sw=4 sts=4 +getHTMLDefinition(); + + $excludes_enabled = !$config->get('Core.DisableExcludes'); + + // setup the context variable 'IsInline', for chameleon processing + // is 'false' when we are not inline, 'true' when it must always + // be inline, and an integer when it is inline for a certain + // branch of the document tree + $is_inline = $definition->info_parent_def->descendants_are_inline; + $context->register('IsInline', $is_inline); + + // setup error collector + $e =& $context->get('ErrorCollector', true); + + //####################################################################// + // Loop initialization + + // stack that contains all elements that are excluded + // it is organized by parent elements, similar to $stack, + // but it is only populated when an element with exclusions is + // processed, i.e. there won't be empty exclusions. + $exclude_stack = array($definition->info_parent_def->excludes); + + // variable that contains the start token while we are processing + // nodes. This enables error reporting to do its job + $node = $top_node; + // dummy token + list($token, $d) = $node->toTokenPair(); + $context->register('CurrentNode', $node); + $context->register('CurrentToken', $token); + + //####################################################################// + // Loop + + // We need to implement a post-order traversal iteratively, to + // avoid running into stack space limits. This is pretty tricky + // to reason about, so we just manually stack-ify the recursive + // variant: + // + // function f($node) { + // foreach ($node->children as $child) { + // f($child); + // } + // validate($node); + // } + // + // Thus, we will represent a stack frame as array($node, + // $is_inline, stack of children) + // e.g. array_reverse($node->children) - already processed + // children. + + $parent_def = $definition->info_parent_def; + $stack = array( + array($top_node, + $parent_def->descendants_are_inline, + $parent_def->excludes, // exclusions + 0) + ); + + while (!empty($stack)) { + list($node, $is_inline, $excludes, $ix) = array_pop($stack); + // recursive call + $go = false; + $def = empty($stack) ? $definition->info_parent_def : $definition->info[$node->name]; + while (isset($node->children[$ix])) { + $child = $node->children[$ix++]; + if ($child instanceof HTMLPurifier_Node_Element) { + $go = true; + $stack[] = array($node, $is_inline, $excludes, $ix); + $stack[] = array($child, + // ToDo: I don't think it matters if it's def or + // child_def, but double check this... + $is_inline || $def->descendants_are_inline, + empty($def->excludes) ? $excludes + : array_merge($excludes, $def->excludes), + 0); + break; + } + }; + if ($go) continue; + list($token, $d) = $node->toTokenPair(); + // base case + if ($excludes_enabled && isset($excludes[$node->name])) { + $node->dead = true; + if ($e) $e->send(E_ERROR, 'Strategy_FixNesting: Node excluded'); + } else { + // XXX I suppose it would be slightly more efficient to + // avoid the allocation here and have children + // strategies handle it + $children = array(); + foreach ($node->children as $child) { + if (!$child->dead) $children[] = $child; + } + $result = $def->child->validateChildren($children, $config, $context); + if ($result === true) { + // nop + $node->children = $children; + } elseif ($result === false) { + $node->dead = true; + if ($e) $e->send(E_ERROR, 'Strategy_FixNesting: Node removed'); + } else { + $node->children = $result; + if ($e) { + // XXX This will miss mutations of internal nodes. Perhaps defer to the child validators + if (empty($result) && !empty($children)) { + $e->send(E_ERROR, 'Strategy_FixNesting: Node contents removed'); + } else if ($result != $children) { + $e->send(E_WARNING, 'Strategy_FixNesting: Node reorganized'); + } + } + } + } + } + + //####################################################################// + // Post-processing + + // remove context variables + $context->destroy('IsInline'); + $context->destroy('CurrentNode'); + $context->destroy('CurrentToken'); + + //####################################################################// + // Return + + return HTMLPurifier_Arborize::flatten($node, $config, $context); + } +} + +// vim: et sw=4 sts=4 diff --git a/lib/purifier/HTMLPurifier/Strategy/MakeWellFormed.php b/lib/purifier/HTMLPurifier/Strategy/MakeWellFormed.php index 144122f0ed..a6eb09e453 100644 --- a/lib/purifier/HTMLPurifier/Strategy/MakeWellFormed.php +++ b/lib/purifier/HTMLPurifier/Strategy/MakeWellFormed.php @@ -1,659 +1,659 @@ -getHTMLDefinition(); - - // local variables - $generator = new HTMLPurifier_Generator($config, $context); - $escape_invalid_tags = $config->get('Core.EscapeInvalidTags'); - // used for autoclose early abortion - $global_parent_allowed_elements = $definition->info_parent_def->child->getAllowedElements($config); - $e = $context->get('ErrorCollector', true); - $i = false; // injector index - list($zipper, $token) = HTMLPurifier_Zipper::fromArray($tokens); - if ($token === NULL) { - return array(); - } - $reprocess = false; // whether or not to reprocess the same token - $stack = array(); - - // member variables - $this->stack =& $stack; - $this->tokens =& $tokens; - $this->token =& $token; - $this->zipper =& $zipper; - $this->config = $config; - $this->context = $context; - - // context variables - $context->register('CurrentNesting', $stack); - $context->register('InputZipper', $zipper); - $context->register('CurrentToken', $token); - - // -- begin INJECTOR -- - - $this->injectors = array(); - - $injectors = $config->getBatch('AutoFormat'); - $def_injectors = $definition->info_injector; - $custom_injectors = $injectors['Custom']; - unset($injectors['Custom']); // special case - foreach ($injectors as $injector => $b) { - // XXX: Fix with a legitimate lookup table of enabled filters - if (strpos($injector, '.') !== false) { - continue; - } - $injector = "HTMLPurifier_Injector_$injector"; - if (!$b) { - continue; - } - $this->injectors[] = new $injector; - } - foreach ($def_injectors as $injector) { - // assumed to be objects - $this->injectors[] = $injector; - } - foreach ($custom_injectors as $injector) { - if (!$injector) { - continue; - } - if (is_string($injector)) { - $injector = "HTMLPurifier_Injector_$injector"; - $injector = new $injector; - } - $this->injectors[] = $injector; - } - - // give the injectors references to the definition and context - // variables for performance reasons - foreach ($this->injectors as $ix => $injector) { - $error = $injector->prepare($config, $context); - if (!$error) { - continue; - } - array_splice($this->injectors, $ix, 1); // rm the injector - trigger_error("Cannot enable {$injector->name} injector because $error is not allowed", E_USER_WARNING); - } - - // -- end INJECTOR -- - - // a note on reprocessing: - // In order to reduce code duplication, whenever some code needs - // to make HTML changes in order to make things "correct", the - // new HTML gets sent through the purifier, regardless of its - // status. This means that if we add a start token, because it - // was totally necessary, we don't have to update nesting; we just - // punt ($reprocess = true; continue;) and it does that for us. - - // isset is in loop because $tokens size changes during loop exec - for (;; - // only increment if we don't need to reprocess - $reprocess ? $reprocess = false : $token = $zipper->next($token)) { - - // check for a rewind - if (is_int($i)) { - // possibility: disable rewinding if the current token has a - // rewind set on it already. This would offer protection from - // infinite loop, but might hinder some advanced rewinding. - $rewind_offset = $this->injectors[$i]->getRewindOffset(); - if (is_int($rewind_offset)) { - for ($j = 0; $j < $rewind_offset; $j++) { - if (empty($zipper->front)) break; - $token = $zipper->prev($token); - // indicate that other injectors should not process this token, - // but we need to reprocess it. See Note [Injector skips] - unset($token->skip[$i]); - $token->rewind = $i; - if ($token instanceof HTMLPurifier_Token_Start) { - array_pop($this->stack); - } elseif ($token instanceof HTMLPurifier_Token_End) { - $this->stack[] = $token->start; - } - } - } - $i = false; - } - - // handle case of document end - if ($token === NULL) { - // kill processing if stack is empty - if (empty($this->stack)) { - break; - } - - // peek - $top_nesting = array_pop($this->stack); - $this->stack[] = $top_nesting; - - // send error [TagClosedSuppress] - if ($e && !isset($top_nesting->armor['MakeWellFormed_TagClosedError'])) { - $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by document end', $top_nesting); - } - - // append, don't splice, since this is the end - $token = new HTMLPurifier_Token_End($top_nesting->name); - - // punt! - $reprocess = true; - continue; - } - - //echo '
'; printZipper($zipper, $token);//printTokens($this->stack); - //flush(); - - // quick-check: if it's not a tag, no need to process - if (empty($token->is_tag)) { - if ($token instanceof HTMLPurifier_Token_Text) { - foreach ($this->injectors as $i => $injector) { - if (isset($token->skip[$i])) { - // See Note [Injector skips] - continue; - } - if ($token->rewind !== null && $token->rewind !== $i) { - continue; - } - // XXX fuckup - $r = $token; - $injector->handleText($r); - $token = $this->processToken($r, $i); - $reprocess = true; - break; - } - } - // another possibility is a comment - continue; - } - - if (isset($definition->info[$token->name])) { - $type = $definition->info[$token->name]->child->type; - } else { - $type = false; // Type is unknown, treat accordingly - } - - // quick tag checks: anything that's *not* an end tag - $ok = false; - if ($type === 'empty' && $token instanceof HTMLPurifier_Token_Start) { - // claims to be a start tag but is empty - $token = new HTMLPurifier_Token_Empty( - $token->name, - $token->attr, - $token->line, - $token->col, - $token->armor - ); - $ok = true; - } elseif ($type && $type !== 'empty' && $token instanceof HTMLPurifier_Token_Empty) { - // claims to be empty but really is a start tag - // NB: this assignment is required - $old_token = $token; - $token = new HTMLPurifier_Token_End($token->name); - $token = $this->insertBefore( - new HTMLPurifier_Token_Start($old_token->name, $old_token->attr, $old_token->line, $old_token->col, $old_token->armor) - ); - // punt (since we had to modify the input stream in a non-trivial way) - $reprocess = true; - continue; - } elseif ($token instanceof HTMLPurifier_Token_Empty) { - // real empty token - $ok = true; - } elseif ($token instanceof HTMLPurifier_Token_Start) { - // start tag - - // ...unless they also have to close their parent - if (!empty($this->stack)) { - - // Performance note: you might think that it's rather - // inefficient, recalculating the autoclose information - // for every tag that a token closes (since when we - // do an autoclose, we push a new token into the - // stream and then /process/ that, before - // re-processing this token.) But this is - // necessary, because an injector can make an - // arbitrary transformations to the autoclosing - // tokens we introduce, so things may have changed - // in the meantime. Also, doing the inefficient thing is - // "easy" to reason about (for certain perverse definitions - // of "easy") - - $parent = array_pop($this->stack); - $this->stack[] = $parent; - - $parent_def = null; - $parent_elements = null; - $autoclose = false; - if (isset($definition->info[$parent->name])) { - $parent_def = $definition->info[$parent->name]; - $parent_elements = $parent_def->child->getAllowedElements($config); - $autoclose = !isset($parent_elements[$token->name]); - } - - if ($autoclose && $definition->info[$token->name]->wrap) { - // Check if an element can be wrapped by another - // element to make it valid in a context (for - // example,