public function testEnablePluginsByArrayWithPaths() { Config::inst()->update('SilverStripe\\Control\\Director', 'alternate_base_url', 'http://mysite.com/subdir'); $c = new TinyMCEConfig(); $c->setTheme('modern'); $c->setOption('language', 'es'); $c->disablePlugins('table', 'emoticons', 'paste', 'code', 'link', 'importcss'); $c->enablePlugins(array('plugin1' => 'mypath/plugin1.js', 'plugin2' => '/anotherbase/mypath/plugin2.js', 'plugin3' => 'https://www.google.com/plugin.js', 'plugin4' => null, 'plugin5' => null)); $attributes = $c->getAttributes(); $config = Convert::json2array($attributes['data-config']); $plugins = $config['external_plugins']; $this->assertNotEmpty($plugins); // Plugin specified via relative url $this->assertContains('plugin1', array_keys($plugins)); $this->assertEquals('http://mysite.com/subdir/mypath/plugin1.js', $plugins['plugin1']); // Plugin specified via root-relative url $this->assertContains('plugin2', array_keys($plugins)); $this->assertEquals('http://mysite.com/anotherbase/mypath/plugin2.js', $plugins['plugin2']); // Plugin specified with absolute url $this->assertContains('plugin3', array_keys($plugins)); $this->assertEquals('https://www.google.com/plugin.js', $plugins['plugin3']); // Plugin specified with standard location $this->assertContains('plugin4', array_keys($plugins)); $this->assertEquals('http://mysite.com/subdir/' . ADMIN_THIRDPARTY_DIR . '/tinymce/plugins/plugin4/plugin.min.js', $plugins['plugin4']); // Check that internal plugins are extractable separately $this->assertEquals(['plugin4', 'plugin5'], $c->getInternalPlugins()); // Test plugins included via gzip compresser HTMLEditorField::config()->update('use_gzip', true); $this->assertEquals(ADMIN_THIRDPARTY_DIR . '/tinymce/tiny_mce_gzip.php?js=1&plugins=plugin4,plugin5&themes=modern&languages=es&diskcache=true&src=true', $c->getScriptURL()); // If gzip is disabled only the core plugin is loaded HTMLEditorField::config()->remove('use_gzip'); $this->assertEquals(ADMIN_THIRDPARTY_DIR . '/tinymce/tinymce.min.js', $c->getScriptURL()); }
/** * Returns a readonly span containing the correct value. * * @param array $properties * * @return string */ public function Field($properties = array()) { $source = ArrayLib::flatten($this->getSource()); $values = $this->getValueArray(); // Get selected values $mapped = array(); foreach ($values as $value) { if (isset($source[$value])) { $mapped[] = $source[$value]; } } // Don't check if string arguments are matching against the source, // as they might be generated HTML diff views instead of the actual values if ($this->value && is_string($this->value) && empty($mapped)) { $mapped = array(trim($this->value)); $values = array(); } if ($mapped) { $attrValue = implode(', ', array_values($mapped)); $attrValue = Convert::raw2xml($attrValue); $inputValue = implode(', ', array_values($values)); } else { $attrValue = '<i>(' . _t('FormField.NONE', 'none') . ')</i>'; $inputValue = ''; } $properties = array_merge($properties, array('AttrValue' => DBField::create_field('HTMLFragment', $attrValue), 'InputValue' => $inputValue)); return parent::Field($properties); }
public function preRequest(HTTPRequest $request, Session $session, DataModel $model) { // Bootstrap session so that Session::get() accesses the right instance $dummyController = new Controller(); $dummyController->setSession($session); $dummyController->setRequest($request); $dummyController->pushCurrent(); // Block non-authenticated users from setting the stage mode if (!Versioned::can_choose_site_stage($request)) { $permissionMessage = sprintf(_t("ContentController.DRAFT_SITE_ACCESS_RESTRICTION", 'You must log in with your CMS password in order to view the draft or archived content. ' . '<a href="%s">Click here to go back to the published site.</a>'), Convert::raw2xml(Controller::join_links(Director::baseURL(), $request->getURL(), "?stage=Live"))); // Force output since RequestFilter::preRequest doesn't support response overriding $response = Security::permissionFailure($dummyController, $permissionMessage); $session->inst_save(); $dummyController->popCurrent(); // Prevent output in testing if (class_exists('SilverStripe\\Dev\\SapphireTest', false) && SapphireTest::is_running_test()) { throw new HTTPResponse_Exception($response); } $response->output(); die; } Versioned::choose_site_stage(); $dummyController->popCurrent(); return true; }
/** * Generate the field ID value * * @param FormField $field * @return string */ public function generateFieldID($field) { if ($form = $field->getForm()) { return sprintf("%s_%s", $this->generateFormID($form), Convert::raw2htmlid($field->getName())); } return Convert::raw2htmlid($field->getName()); }
public function Field($properties = array()) { $config = array('timeformat' => $this->getConfig('timeformat')); $config = array_filter($config); $this->addExtraClass(Convert::raw2json($config)); return parent::Field($properties); }
/** * @return string */ public function getContent() { $doc = clone $this->getDocument(); $xp = new DOMXPath($doc); // If there's no body, the content is empty string if (!$doc->getElementsByTagName('body')->length) { return ''; } // saveHTML Percentage-encodes any URI-based attributes. We don't want this, since it interferes with // shortcodes. So first, save all the attribute values for later restoration. $attrs = array(); $i = 0; foreach ($xp->query('//body//@*') as $attr) { $key = "__HTMLVALUE_" . $i++; $attrs[$key] = $attr->value; $attr->value = $key; } // Then, call saveHTML & extract out the content from the body tag $res = preg_replace(array('/^(.*?)<body>/is', '/<\\/body>(.*?)$/isD'), '', $doc->saveHTML()); // Then replace the saved attributes with their original versions $res = preg_replace_callback('/__HTMLVALUE_(\\d+)/', function ($matches) use($attrs) { return Convert::raw2att($attrs[$matches[0]]); }, $res); // Prevent being encoded as literal utf-8 characters // Possible alternative solution: http://stackoverflow.com/questions/2142120/php-encoding-with-domdocument $from = mb_convert_encoding(' ', 'utf-8', 'html-entities'); $res = str_replace($from, ' ', $res); return $res; }
public function processRecord($record, $columnMap, &$results, $preview = false) { $objID = parent::processRecord($record, $columnMap, $results, $preview); $_cache_groupByCode = array(); // Add to predefined groups /** @var Member $member */ $member = DataObject::get_by_id($this->objectClass, $objID); foreach ($this->groups as $group) { // TODO This isnt the most memory effective way to add members to a group $member->Groups()->add($group); } // Add to groups defined in CSV if (isset($record['Groups']) && $record['Groups']) { $groupCodes = explode(',', $record['Groups']); foreach ($groupCodes as $groupCode) { $groupCode = Convert::raw2url($groupCode); if (!isset($_cache_groupByCode[$groupCode])) { $group = Group::get()->filter('Code', $groupCode)->first(); if (!$group) { $group = new Group(); $group->Code = $groupCode; $group->Title = $groupCode; $group->write(); } $member->Groups()->add($group); $_cache_groupByCode[$groupCode] = $group; } } } $member->destroy(); unset($member); return $objID; }
/** * Get raw HTML for image markup * * @param File $file * @return string */ protected function getIconMarkup($file) { if (!$file) { return null; } $previewLink = Convert::raw2att($file->PreviewLink()); return "<img src=\"{$previewLink}\" class=\"editor__thumbnail\" />"; }
public function Field($properties = array()) { if ($this->valueObj) { $val = Convert::raw2xml($this->valueObj->toString($this->getConfig('timeformat'))); } else { // TODO Localization $val = '<i>(not set)</i>'; } return "<span class=\"readonly\" id=\"" . $this->ID() . "\">{$val}</span>"; }
/** * overloaded to display the correctly formated value for this datatype * * @param array $properties * @return string */ public function Field($properties = array()) { if ($this->value) { $val = Convert::raw2xml($this->value); $val = _t('CurrencyField.CURRENCYSYMBOL', '$') . number_format(preg_replace('/[^0-9.]/', "", $val), 2); $valforInput = Convert::raw2att($val); } else { $valforInput = ''; } return "<input class=\"text\" type=\"text\" disabled=\"disabled\"" . " name=\"" . $this->name . "\" value=\"" . $valforInput . "\" />"; }
/** * Create a new PolymorphicHasManyList relation list. * * @param string $dataClass The class of the DataObjects that this will list. * @param string $foreignField The name of the composite foreign relation field. Used * to generate the ID and Class foreign keys. * @param string $foreignClass Name of the class filter this relation is filtered against */ function __construct($dataClass, $foreignField, $foreignClass) { // Set both id foreign key (as in HasManyList) and the class foreign key parent::__construct($dataClass, "{$foreignField}ID"); $this->classForeignKey = "{$foreignField}Class"; // Ensure underlying DataQuery globally references the class filter $this->dataQuery->setQueryParam('Foreign.Class', $foreignClass); // For queries with multiple foreign IDs (such as that generated by // DataList::relation) the filter must be generalised to filter by subclasses $classNames = Convert::raw2sql(ClassInfo::subclassesFor($foreignClass)); $this->dataQuery->where(sprintf("\"{$this->classForeignKey}\" IN ('%s')", implode("', '", $classNames))); }
/** * Overloaded to display the correctly formated value for this datatype * * @param array $properties * @return string */ public function Field($properties = array()) { if ($this->value) { $val = Convert::raw2xml($this->value); $val = _t('CurrencyField.CURRENCYSYMBOL', '$') . number_format(preg_replace('/[^0-9.]/', "", $val), 2); $valforInput = Convert::raw2att($val); } else { $val = '<i>' . _t('CurrencyField.CURRENCYSYMBOL', '$') . '0.00</i>'; $valforInput = ''; } return "<span class=\"readonly " . $this->extraClass() . "\" id=\"" . $this->ID() . "\">{$val}</span>" . "<input type=\"hidden\" name=\"" . $this->name . "\" value=\"" . $valforInput . "\" />"; }
protected function getSpecsMarkup($record) { if (!$record || !$record->isInDB()) { return null; } /** * Can remove .label and .label-info when Bootstrap has been updated to BS4 Beta * .label is being replaced with .tag */ $versionTag = sprintf('<span class="label label-info tag tag-info">v.%s</span>', $record->Version); $agoTag = sprintf('%s <time class="relative-time" title="%s">%s</time>', $record->WasPublished ? _t('SilverStripe\\AssetAdmin\\Forms\\FileHistoryFormFactory.PUBLISHED', 'Published') : _t('SilverStripe\\AssetAdmin\\Forms\\FileHistoryFormFactory.SAVED', 'Saved'), Convert::raw2att($record->LastEdited), Convert::raw2xml($record->dbObject('LastEdited')->Ago())); return sprintf('<div class="editor__specs">%s %s, %s %s</div>', $versionTag, $agoTag, $record->getSize(), $this->getStatusFlagMarkup($record)); }
/** * Create the "custom" selection field option * * @param bool $isChecked True if this is checked * @param bool $odd Is odd striped * @return ArrayData */ protected function getCustomFieldOption($isChecked, $odd) { // Add "custom" input field $option = $this->getFieldOption(self::CUSTOM_OPTION, _t('MemberDatetimeOptionsetField.Custom', 'Custom'), $odd); $option->setField('isChecked', $isChecked); $option->setField('CustomName', $this->getName() . '[Custom]'); $option->setField('CustomValue', $this->Value()); if ($this->Value()) { $preview = Convert::raw2xml($this->previewFormat($this->Value())); $option->setField('CustomPreview', $preview); $option->setField('CustomPreviewLabel', _t('MemberDatetimeOptionsetField.Preview', 'Preview')); } return $option; }
public function Field($properties = array()) { if ($this->valueObj) { if ($this->valueObj->isToday()) { $val = Convert::raw2xml($this->valueObj->toString($this->getConfig('dateformat')) . ' (' . _t('DateField.TODAY', 'today') . ')'); } else { $df = new DBDate($this->name); $df->setValue($this->dataValue()); $val = Convert::raw2xml($this->valueObj->toString($this->getConfig('dateformat')) . ', ' . $df->Ago()); } } else { $val = '<i>(' . _t('DateField.NOTSET', 'not set') . ')</i>'; } return "<span class=\"readonly\" id=\"" . $this->ID() . "\">{$val}</span>"; }
public function run(SS_List $records) { $status = array('modified' => array(), 'deleted' => array()); foreach ($records as $record) { $id = $record->ID; // Perform the action if ($record->canDelete()) { $record->delete(); } $status['deleted'][$id] = array(); $record->destroy(); unset($record); } return Convert::raw2json($status); }
/** * Gets the list of options to render in this formfield * * @return ArrayList */ public function getOptions() { $selectedValues = $this->getValueArray(); $defaultItems = $this->getDefaultItems(); // Generate list of options to display $odd = false; $formID = $this->ID(); $options = new ArrayList(); foreach ($this->getSource() as $itemValue => $title) { $itemID = Convert::raw2htmlid("{$formID}_{$itemValue}"); $odd = !$odd; $extraClass = $odd ? 'odd' : 'even'; $extraClass .= ' val' . preg_replace('/[^a-zA-Z0-9\\-\\_]/', '_', $itemValue); $itemChecked = in_array($itemValue, $selectedValues) || in_array($itemValue, $defaultItems); $itemDisabled = $this->isDisabled() || in_array($itemValue, $defaultItems); $options->push(new ArrayData(array('ID' => $itemID, 'Class' => $extraClass, 'Name' => "{$this->name}[{$itemValue}]", 'Value' => $itemValue, 'Title' => $title, 'isChecked' => $itemChecked, 'isDisabled' => $itemDisabled))); } $this->extend('updateGetOptions', $options); return $options; }
/** * Helper method for responding to a back action request * @param string $successMessage The message to return as a notification. * Can have up to two %d's in it. The first will be replaced by the number of successful * changes, the second by the number of failures * @param array $status A status array like batchactions builds. Should be * key => value pairs, the key can be any string: "error" indicates errors, anything * else indicates a type of success. The value is an array. We don't care what's in it, * we just use count($value) to find the number of items that succeeded or failed * @return string */ public function response($successMessage, $status) { $count = 0; $errors = 0; foreach ($status as $k => $v) { switch ($k) { case 'error': $errors += count($v); break; case 'success': $count += count($v); break; } } $response = Controller::curr()->getResponse(); if ($response) { $response->setStatusCode(200, sprintf($successMessage, $count, $errors)); } return Convert::raw2json($status); }
/** * Redirects the user to the external login page * * @return HTTPResponse */ protected function redirectToExternalLogin() { $loginURL = Security::create()->Link('login'); $loginURLATT = Convert::raw2att($loginURL); $loginURLJS = Convert::raw2js($loginURL); $message = _t('CMSSecurity.INVALIDUSER', '<p>Invalid user. <a target="_top" href="{link}">Please re-authenticate here</a> to continue.</p>', 'Message displayed to user if their session cannot be restored', array('link' => $loginURLATT)); $response = $this->getResponse(); $response->setStatusCode(200); $response->setBody(<<<PHP <!DOCTYPE html> <html><body> {$message} <script type="application/javascript"> setTimeout(function(){top.location.href = "{$loginURLJS}";}, 0); </script> </body></html> PHP ); $this->setResponse($response); return $response; }
/** * @uses FormField::name_to_label() * * @param string $name Identifier of the tab, without characters like dots or spaces * @param string|FormField $titleOrField Natural language title of the tabset, or first tab. * If its left out, the class uses {@link FormField::name_to_label()} to produce a title * from the {@link $name} parameter. * @param FormField ...$fields All following parameters are inserted as children to this tab */ public function __construct($name, $titleOrField = null, $fields = null) { if (!is_string($name)) { throw new InvalidArgumentException('Invalid string parameter for $name'); } // Get following arguments $fields = func_get_args(); array_shift($fields); // Detect title from second argument, if it is a string if ($titleOrField && is_string($titleOrField)) { $title = $titleOrField; array_shift($fields); } else { $title = static::name_to_label($name); } // Remaining arguments are child fields parent::__construct($fields); // Assign name and title (not assigned by parent constructor) $this->setName($name); $this->setTitle($title); $this->setID(Convert::raw2htmlid($name)); }
function testSearch() { $team1 = $this->objFromFixture('GridFieldTest_Team', 'team1'); $team2 = $this->objFromFixture('GridFieldTest_Team', 'team2'); $response = $this->get('GridFieldAddExistingAutocompleterTest_Controller'); $this->assertFalse($response->isError()); $parser = new CSSContentParser($response->getBody()); $btns = $parser->getBySelector('.grid-field .action_gridfield_relationfind'); $response = $this->post('GridFieldAddExistingAutocompleterTest_Controller/Form/field/testfield/search' . '/?gridfield_relationsearch=Team 2', array((string) $btns[0]['name'] => 1)); $this->assertFalse($response->isError()); $result = Convert::json2array($response->getBody()); $this->assertEquals(1, count($result)); $this->assertEquals(array(array('label' => 'Team 2', 'value' => 'Team 2', 'id' => $team2->ID)), $result); $response = $this->post('GridFieldAddExistingAutocompleterTest_Controller/Form/field/testfield/' . 'search/?gridfield_relationsearch=Heather', array((string) $btns[0]['name'] => 1)); $this->assertFalse($response->isError()); $result = Convert::json2array($response->getBody()); $this->assertEquals(1, count($result), "The relational filter did not work"); $response = $this->post('GridFieldAddExistingAutocompleterTest_Controller/Form/field/testfield/search' . '/?gridfield_relationsearch=Unknown', array((string) $btns[0]['name'] => 1)); $this->assertFalse($response->isError()); $result = Convert::json2array($response->getBody()); $this->assertEmpty($result, 'The output is either an empty array or boolean FALSE'); }
/** * Returns a version of a title suitable for insertion into an HTML attribute. * * @return string */ public function attrValue() { return Convert::raw2att($this->value); }
public function testCacheFilename() { $image = $this->objFromFixture('SilverStripe\\Assets\\Image', 'imageWithoutTitle'); $imageFirst = $image->Pad(200, 200, 'CCCCCC'); $imageFilename = $imageFirst->getURL(); // Encoding of the arguments is duplicated from cacheFilename $neededPart = 'Pad' . Convert::base64url_encode(array(200, 200, 'CCCCCC')); $this->assertContains($neededPart, $imageFilename, 'Filename for cached image is correctly generated'); }
/** * @param string $varname * @param string $varvalue * @param null|string $currentURL * * @return string */ public static function RAW_setGetVar($varname, $varvalue, $currentURL = null) { $url = self::setGetVar($varname, $varvalue, $currentURL); return Convert::xml2raw($url); }
/** * Test that UploadField:overwriteWarning cannot overwrite Upload:replaceFile */ public function testConfigOverwriteWarningCannotRelaceFiles() { Upload::config()->replaceFile = false; UploadField::config()->defaultConfig = array_merge(UploadField::config()->defaultConfig, array('overwriteWarning' => true)); $tmpFileName = 'testUploadBasic.txt'; $response = $this->mockFileUpload('NoRelationField', $tmpFileName); $this->assertFalse($response->isError()); $responseData = Convert::json2array($response->getBody()); $uploadedFile = DataObject::get_by_id('SilverStripe\\Assets\\File', (int) $responseData[0]['id']); $this->assertTrue(is_object($uploadedFile), 'The file object is created'); $this->assertFileExists(AssetStoreTest_SpyStore::getLocalPath($uploadedFile)); $tmpFileName = 'testUploadBasic.txt'; $response = $this->mockFileUpload('NoRelationField', $tmpFileName); $this->assertFalse($response->isError()); $responseData = Convert::json2array($response->getBody()); $uploadedFile2 = DataObject::get_by_id('SilverStripe\\Assets\\File', (int) $responseData[0]['id']); $this->assertTrue(is_object($uploadedFile2), 'The file object is created'); $this->assertFileExists(AssetStoreTest_SpyStore::getLocalPath($uploadedFile2)); $this->assertTrue($uploadedFile->Filename !== $uploadedFile2->Filename, 'Filename is not the same'); $this->assertTrue($uploadedFile->ID !== $uploadedFile2->ID, 'File database record is not the same'); }
/** * The process() method handles the "meat" of the template processing. * * It takes care of caching the output (via {@link Cache}), as well as * replacing the special "$Content" and "$Layout" placeholders with their * respective subtemplates. * * The method injects extra HTML in the header via {@link Requirements::includeInHTML()}. * * Note: You can call this method indirectly by {@link ViewableData->renderWith()}. * * @param ViewableData $item * @param array|null $arguments Arguments to an included template * @param ViewableData $inheritedScope The current scope of a parent template including a sub-template * @return DBHTMLText Parsed template output. */ public function process($item, $arguments = null, $inheritedScope = null) { SSViewer::$topLevel[] = $item; $template = $this->chosen; $cacheFile = TEMP_FOLDER . "/.cache" . str_replace(array('\\', '/', ':'), '.', Director::makeRelative(realpath($template))); $lastEdited = filemtime($template); if (!file_exists($cacheFile) || filemtime($cacheFile) < $lastEdited) { $content = file_get_contents($template); $content = $this->parseTemplateContent($content, $template); $fh = fopen($cacheFile, 'w'); fwrite($fh, $content); fclose($fh); } $underlay = array('I18NNamespace' => basename($template)); // Makes the rendered sub-templates available on the parent item, // through $Content and $Layout placeholders. foreach (array('Content', 'Layout') as $subtemplate) { $sub = null; if (isset($this->subTemplates[$subtemplate])) { $sub = $this->subTemplates[$subtemplate]; } elseif (!is_array($this->templates)) { $sub = ['type' => $subtemplate, $this->templates]; } elseif (!array_key_exists('type', $this->templates) || !$this->templates['type']) { $sub = array_merge($this->templates, ['type' => $subtemplate]); } if ($sub) { $subtemplateViewer = clone $this; // Disable requirements - this will be handled by the parent template $subtemplateViewer->includeRequirements(false); // Select the right template $subtemplateViewer->setTemplate($sub); if ($subtemplateViewer->exists()) { $underlay[$subtemplate] = $subtemplateViewer->process($item, $arguments); } } } $output = $this->includeGeneratedTemplate($cacheFile, $item, $arguments, $underlay, $inheritedScope); if ($this->includeRequirements) { $output = Requirements::includeInHTML($output); } array_pop(SSViewer::$topLevel); // If we have our crazy base tag, then fix # links referencing the current page. $rewrite = SSViewer::config()->get('rewrite_hash_links'); if ($this->rewriteHashlinks && $rewrite) { if (strpos($output, '<base') !== false) { if ($rewrite === 'php') { $thisURLRelativeToBase = "<?php echo \\SilverStripe\\Core\\Convert::raw2att(preg_replace(\"/^(\\\\/)+/\", \"/\", \$_SERVER['REQUEST_URI'])); ?>"; } else { $thisURLRelativeToBase = Convert::raw2att(preg_replace("/^(\\/)+/", "/", $_SERVER['REQUEST_URI'])); } $output = preg_replace('/(<a[^>]+href *= *)"#/i', '\\1"' . $thisURLRelativeToBase . '#', $output); } } return DBField::create_field('HTMLFragment', $output); }
public function testSummaryEndings() { $cases = array('...', ' -> more', ''); $orig = '<p>Cut it off, cut it off</p>'; $match = 'Cut it off, cut'; foreach ($cases as $add) { $textObj = DBField::create_field('HTMLFragment', $orig); $result = $textObj->obj('Summary', [4, $add])->forTemplate(); $this->assertEquals($match . Convert::raw2xml($add), $result); } }
/** * Processing that occurs before a form is executed. * * This includes form validation, if it fails, we redirect back * to the form with appropriate error messages. * Always return true if the current form action is exempt from validation * * Triggered through {@link httpSubmission()}. * * Note that CSRF protection takes place in {@link httpSubmission()}, * if it fails the form data will never reach this method. * * @return boolean */ public function validate() { $action = $this->buttonClicked(); if ($action && $this->actionIsValidationExempt($action)) { return true; } if ($this->validator) { $errors = $this->validator->validate(); if ($errors) { // Load errors into session and post back $data = $this->getData(); // Encode validation messages as XML before saving into session state // As per Form::addErrorMessage() $errors = array_map(function ($error) { // Encode message as XML by default if ($error['message'] instanceof DBField) { $error['message'] = $error['message']->forTemplate(); } else { $error['message'] = Convert::raw2xml($error['message']); } return $error; }, $errors); Session::set("FormInfo.{$this->FormName()}.errors", $errors); Session::set("FormInfo.{$this->FormName()}.data", $data); return false; } } return true; }
/** * Simple conversion of HTML to plaintext. * * @param string $data Input data * @param bool $preserveLinks * @param int $wordWrap * @param array $config * @return string */ public static function html2raw($data, $preserveLinks = false, $wordWrap = 0, $config = null) { $defaultConfig = array('PreserveLinks' => false, 'ReplaceBoldAsterisk' => true, 'CompressWhitespace' => true, 'ReplaceImagesWithAlt' => true); if (isset($config)) { $config = array_merge($defaultConfig, $config); } else { $config = $defaultConfig; } $data = preg_replace("/<style([^A-Za-z0-9>][^>]*)?>.*?<\\/style[^>]*>/is", "", $data); $data = preg_replace("/<script([^A-Za-z0-9>][^>]*)?>.*?<\\/script[^>]*>/is", "", $data); if ($config['ReplaceBoldAsterisk']) { $data = preg_replace('%<(strong|b)( [^>]*)?>|</(strong|b)>%i', '*', $data); } // Expand hyperlinks if (!$preserveLinks && !$config['PreserveLinks']) { $data = preg_replace_callback('/<a[^>]*href\\s*=\\s*"([^"]*)">(.*?)<\\/a>/i', function ($matches) { return Convert::html2raw($matches[2]) . "[{$matches['1']}]"; }, $data); $data = preg_replace_callback('/<a[^>]*href\\s*=\\s*([^ ]*)>(.*?)<\\/a>/i', function ($matches) { return Convert::html2raw($matches[2]) . "[{$matches['1']}]"; }, $data); } // Replace images with their alt tags if ($config['ReplaceImagesWithAlt']) { $data = preg_replace('/<img[^>]*alt *= *"([^"]*)"[^>]*>/i', ' \\1 ', $data); $data = preg_replace('/<img[^>]*alt *= *([^ ]*)[^>]*>/i', ' \\1 ', $data); } // Compress whitespace if ($config['CompressWhitespace']) { $data = preg_replace("/\\s+/", " ", $data); } // Parse newline tags $data = preg_replace("/\\s*<[Hh][1-6]([^A-Za-z0-9>][^>]*)?> */", "\n\n", $data); $data = preg_replace("/\\s*<[Pp]([^A-Za-z0-9>][^>]*)?> */", "\n\n", $data); $data = preg_replace("/\\s*<[Dd][Ii][Vv]([^A-Za-z0-9>][^>]*)?> */", "\n\n", $data); $data = preg_replace("/\n\n\n+/", "\n\n", $data); $data = preg_replace("/<[Bb][Rr]([^A-Za-z0-9>][^>]*)?> */", "\n", $data); $data = preg_replace("/<[Tt][Rr]([^A-Za-z0-9>][^>]*)?> */", "\n", $data); $data = preg_replace("/<\\/[Tt][Dd]([^A-Za-z0-9>][^>]*)?> */", " ", $data); $data = preg_replace('/<\\/p>/i', "\n\n", $data); // Replace HTML entities $data = html_entity_decode($data, ENT_QUOTES, 'UTF-8'); // Remove all tags (but optionally keep links) // strip_tags seemed to be restricting the length of the output // arbitrarily. This essentially does the same thing. if (!$preserveLinks && !$config['PreserveLinks']) { $data = preg_replace('/<\\/?[^>]*>/', '', $data); } else { $data = strip_tags($data, '<a>'); } // Wrap if ($wordWrap) { $data = wordwrap(trim($data), $wordWrap); } return trim($data); }
/** * @param string $from * @param string $to * @param bool $escape * @return string */ public static function compareHTML($from, $to, $escape = false) { // First split up the content into words and tags $set1 = self::getHTMLChunks($from); $set2 = self::getHTMLChunks($to); // Diff that $diff = new Diff($set1, $set2); $tagStack[1] = $tagStack[2] = 0; $rechunked[1] = $rechunked[2] = array(); // Go through everything, converting edited tags (and their content) into single chunks. Otherwise // the generated HTML gets crusty foreach ($diff->edits as $edit) { $lookForTag = false; $stuffFor = []; switch ($edit->type) { case 'copy': $lookForTag = false; $stuffFor[1] = $edit->orig; $stuffFor[2] = $edit->orig; break; case 'change': $lookForTag = true; $stuffFor[1] = $edit->orig; $stuffFor[2] = $edit->final; break; case 'add': $lookForTag = true; $stuffFor[1] = null; $stuffFor[2] = $edit->final; break; case 'delete': $lookForTag = true; $stuffFor[1] = $edit->orig; $stuffFor[2] = null; break; } foreach ($stuffFor as $listName => $chunks) { if ($chunks) { foreach ($chunks as $item) { // $tagStack > 0 indicates that we should be tag-building if ($tagStack[$listName]) { $rechunked[$listName][sizeof($rechunked[$listName]) - 1] .= ' ' . $item; } else { $rechunked[$listName][] = $item; } if ($lookForTag && !$tagStack[$listName] && isset($item[0]) && $item[0] == "<" && substr($item, 0, 2) != "</") { $tagStack[$listName] = 1; } else { if ($tagStack[$listName]) { if (substr($item, 0, 2) == "</") { $tagStack[$listName]--; } else { if (isset($item[0]) && $item[0] == "<") { $tagStack[$listName]++; } } } } } } } } // Diff the re-chunked data, turning it into maked up HTML $diff = new Diff($rechunked[1], $rechunked[2]); $content = ''; foreach ($diff->edits as $edit) { $orig = $escape ? Convert::raw2xml($edit->orig) : $edit->orig; $final = $escape ? Convert::raw2xml($edit->final) : $edit->final; switch ($edit->type) { case 'copy': $content .= " " . implode(" ", $orig) . " "; break; case 'change': $content .= " <ins>" . implode(" ", $final) . "</ins> "; $content .= " <del>" . implode(" ", $orig) . "</del> "; break; case 'add': $content .= " <ins>" . implode(" ", $final) . "</ins> "; break; case 'delete': $content .= " <del>" . implode(" ", $orig) . "</del> "; break; } } return self::cleanHTML($content); }