Source of file TkiRequirements_Backend.php
Size: 27,722 Bytes - Last Modified: 2021-12-23T10:52:21+00:00
/var/www/docs.ssmods.com/process/src/code/TkiRequirements_Backend.php
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959 | <?php class TkiRequirements_Backend extends Requirements_Backend { /** * Generated HTML which is inserted into document * @var string */ protected $requirementsHtml = array( 'head' => '', 'body' => '', 'bottom' => '' ); /** * Default priority * @config * @var int */ private static $default_priority = 50; /* * ------------------------------------------------------------------------- * Public methods * ------------------------------------------------------------------------- */ /** * Include CSS file(s) * @param array|string $files * @see TkiRequirements_Backend::requireFiles() for formats. * @param string $media - Target media * @param int $priority - Load priority (The higher the number, the greater priority - generally between 0-100) * media => (string) - stylesheet media */ public function stylesheets($files, $media = null, $priority = null) { // Check if(empty($files)) { return; } $options = compact('media','priority'); // Add to queue if(is_array($files)) { $this->requireFiles($files,$options); } else { $this->css[$files] = $options; } } /** * Include JavaScript file(s) * @param array|string $files * @see TkiRequirements_Backend::requireFiles() for formats. * @param string $location - (head|body|bottom) location scripts are inserted in the document * @param int $priority - Load priority (The higher the number, the greater priority - generally between 0-100) */ public function js($files, $location = null, $priority = null) { // Check if(empty($files)) { return; } $options = compact('priority', 'location'); // Add to queue if(is_array($files)) { $this->requireFiles($files,$options); } else { $this->javascript[$files] = $options; } } /** * Include custom script * @param string $script - Script content (without the script tags) * @param int $uniquenessID - A unique ID that ensures a piece of code is only added once * @param int $priority - Load priority (The higher the number, the greater priority - generally between 0-100) * @param string $location - (head|body|bottom) location scripts are inserted in the document */ public function script($script, $uniquenessID = null, $location = null, $priority = null) { // Check if(empty($script)) { return; } $options = compact('priority', 'location'); if($uniquenessID) { $this->customScript[$uniquenessID] = compact('script','options'); } else { $this->customScript[] = compact('script','options'); } } /** * Override parent method to make use of options * @param string $script [description] * @param int $uniquenessID [description] */ public function customScript($script, $uniquenessID = null) { $this->script($script,$uniquenessID); } /** * Include custom styles * @param string $styles - Script content (without the script tags) * @param int $uniquenessID - A unique ID that ensures a piece of code is only added once * @param int $media - Stylesheet media */ public function styles($styles, $uniquenessID = null, $media = null) { // Check if(empty($styles)) { return; } $options = compact('media'); if($uniquenessID) { $this->customCSS[$uniquenessID] = compact('styles','options'); } else { $test = compact('styles','options'); $this->customCSS[] = compact('styles','options'); } } /** * Override parent method to make use of options * @param string $script Styles * @param int $uniquenessID */ public function customCSS($script, $uniquenessID = null) { $this->styles($script,$uniquenessID); } /** * Return all registered custom scripts * * @return array */ public function get_custom_scripts() { $requirements = ""; if($this->customScript) { foreach($this->customScript as $arr) { $requirements .= $arr['script'] ."\n"; } } return $requirements; } /** * @see Requirements_Backend::includeInHTML(); * * This overrides to add location and prioritization * * * @param string $templateFile No longer used, only retained for compatibility * @param string $content HTML content that has already been parsed from the $templateFile * through {@link SSViewer} * @return string HTML content augmented with the requirements tags */ public function includeInHTML($templateFile, $content) { // Init/reset $this->requirementsHtml = array( 'head' => '', 'body' => '', 'bottom' => '' ); if( (strpos($content, '</head>') !== false || strpos($content, '</head ') !== false) && ($this->css || $this->javascript || $this->customCSS || $this->customScript || $this->customHeadTags) ) { // Combine files - updates $this->javascript and $this->css $this->process_combined_files(); // Include CSS and JavaScript $this->includeCss(); $this->includeCustomHeadTags(); $this->includeJs(); /* * Insert HTML */ $replacements = array(); /* * Head - Scripts which are inserted before the closing head tag */ if($this->requirementsHtml['head']) { $replacements["/(<\/head>)/i"] = $this->escapeReplacement($this->requirementsHtml['head']) . "\\1"; } /* * Body - Scripts which are inserted in the body, before any existing script tags in the template. * If there are no script tags, they are inserted at the bottom. */ if($this->requirementsHtml['body']) { // Remove all newlines from code to preserve layout $this->requirementsHtml['body'] = $this->removeNewlinesFromCode($this->requirementsHtml['body']); $p2 = stripos($content, '<body'); $p1 = stripos($content, '<script', $p2); $commentTags = array(); $canWriteToBody = ($p1 !== false) && // Check that the script tag is not inside a html comment tag !( preg_match('/.*(?|(<!--)|(-->))/U', $content, $commentTags, 0, $p1) && $commentTags[1] == '-->' ); if ($canWriteToBody) { $content = substr($content, 0, $p1) . $this->requirementsHtml['body'] . substr($content, $p1); } else { $replacements["/(<\/body[^>]*>)/i"] = $this->escapeReplacement($this->requirementsHtml['body']) . "\\1"; } } /* * Bottom - Scripts which are inserted at the bottom of the body, after all other scripts */ if($this->requirementsHtml['bottom']) { // Remove all newlines from code to preserve layout $this->requirementsHtml['bottom'] = $this->removeNewlinesFromCode($this->requirementsHtml['bottom']); if(!array_key_exists("/(<\/body[^>]*>)/i",$replacements)) { $replacements["/(<\/body[^>]*>)/i"] = ''; } $replacements["/(<\/body[^>]*>)/i"] .= $this->escapeReplacement($this->requirementsHtml['bottom']) . "\\1"; } // Insert in content if (!empty($replacements)) { // Replace everything at once (only once) $content = preg_replace(array_keys($replacements), array_values($replacements), $content, 1); } } return $content; } /** * Attach requirements inclusion to X-Include-JS and X-Include-CSS headers on the given * HTTP Response * * @param SS_HTTPResponse $response */ public function include_in_response(SS_HTTPResponse $response) { $this->process_combined_files(); $jsRequirements = array(); $cssRequirements = array(); // JavaScript $js = array_diff_key($this->javascript,$this->blocked); $prioritisedJs = $this->sortByPriority($js); foreach($prioritisedJs as $files) { foreach($files as $file => $options) { $path = $this->path_for_file($file); if($path) { $jsRequirements[] = str_replace(',', '%2C', $path); } } } $response->addHeader('X-Include-JS', implode(',', $jsRequirements)); // CSS $css = array_diff_key($this->css,$this->blocked); $prioritisedCss = $this->sortByPriority($css); foreach($prioritisedCss as $files) { foreach($files as $file => $params) { $path = $this->path_for_file($file); if($path) { $path = str_replace(',', '%2C', $path); $cssRequirements[] = isset($params['media']) ? "$path:##:$params[media]" : $path; } } } $response->addHeader('X-Include-CSS', implode(',', $cssRequirements)); } /** * Sorts array of files according to target location. * @param array $items - Array JS files or script blocks * @return array */ protected function separateJsByLocation($items) { $out = array(); foreach($items as $key => $val) { // In file arrays, each item is composed of $filename => $options. // In script arrays, each item is $index => array('script' => $script, 'options' => $options) if(is_array($val) && isset($val['options'])) { $keyIsFile = false; $options = $val['options']; } else { $keyIsFile = true; $options = $val; } $location = $this->determineLocation($options); if(!isset($out[$location])) { $out[$location] = array(); } if($keyIsFile) { $out[$location][$key] = $options; } else { $out[$location][] = $val; } } return $out; } /** * Determines location for item, based on default or location specified in options. * Overridden when $this->force_js_to_bottom is true or $this->write_js_to_body is false * * @param array $options - Options array with location as one of the keys * @return string (head|body|bottom) */ protected function determineLocation($options) { // Default location is determined by $this->write_js_to_body property. $default = $this->write_js_to_body ? 'body' : 'head'; $location = (is_array($options) && !empty($options['location']) && in_array(strtolower($options['location']),array('head','body','bottom'))) ? strtolower($options['location']) : $default; /* Overrides */ // Force to body if($this->force_js_to_bottom) { $location = 'bottom'; } // Force to head elseif(!$this->write_js_to_body) { $location = 'head'; } return $location; } /** * Sorts files by priority given in file options. Default priority used if not provided. * If two files have the same priority, then the first one takes priority. * @param array $items * If false, it is assumed each array item is $index => array('script' => $script, 'options' => $options) */ protected function sortByPriority($items) { $default = Config::inst()->get(get_class($this),'default_priority'); $out = array($default => array()); foreach($items as $key => $val) { // In file arrays, each item is composed of $filename => $options. // In script arrays, each item is $index => array('script' => $script, 'options' => $options) if(is_array($val) && isset($val['options'])) { $keyIsFile = false; $options = $val['options']; } else { $keyIsFile = true; $options = $val; } $priority = (is_array($options) && isset($options['priority'])) ? (int) $options['priority'] : $default; if(!isset($out[$priority])) { $out[$priority] = array(); } if($keyIsFile) { $out[$priority][$key] = $options; } else { $out[$priority][] = $val; } } krsort($out); return $out; } /** * Creates HTML to include JavaScript, given array of JavaScript items. * Outputs script links or script blocks depending on "external" flag. * @param array $items * @return string */ protected function generateJsElements($items) { $html = ''; foreach($items as $item => $arr) { // Detect custom script if(is_array($arr) && !empty($arr['script'])) { $html .= "<script type=\"text/javascript\">\n//<![CDATA[\n"; $html .= $arr['script'] ."\n"; $html .= "\n//]]>\n</script>\n"; } // External file - $item is filename elseif(is_string($item)) { $path = Convert::raw2xml($this->path_for_file($item)); if($path) { $html .= "<script type=\"text/javascript\" src=\"$path\"></script>\n"; } } } return $html; } /** * Generates HTML for * @param array $arr * @param boolean $external * @return string */ protected function generateCssElements($items) { $html = ''; // External stylesheets foreach($items as $item => $arr) { if(isset($arr['options'])) { $options = $arr['options']; $styles = isset($arr['styles']) ? $arr['styles'] : ''; } else { $options = $arr; } // Detect styles block if(isset($styles)) { $media = (isset($options['media']) && !empty($options['media'])) ? " media=\"{$options['media']}\"" : ""; $html .= "<style type=\"text/css\"{$media}>\n$styles\n</style>\n"; } // External file - $item is filename elseif(is_string($item)) { $path = Convert::raw2xml($this->path_for_file($item)); if($path) { $media = (!empty($options['media'])) ? " media=\"{$options['media']}\"" : ""; $html .= "<link rel=\"stylesheet\" type=\"text/css\"{$media} href=\"$path\" />\n"; } } } return $html; } /** * Sorts provided JavaScript items according to target location and priority, * then adds the generated HTML to $this->requirementsHtml. HTML generation * is performed by helper methods. */ protected function includeJs() { // Filter and check JS $files = array_diff_key($this->javascript,$this->blocked); $customScripts = array_diff_key($this->customScript,$this->blocked); // Add the script blocks to the JavaScript files array $js = array_merge($files,array_values($customScripts)); if(empty($js)) { return; } // Separate by location $separated = $this->separateJsByLocation($js); foreach($separated as $location => $items) { // Prioritise $prioritised = $this->sortByPriority($items); foreach($prioritised as $scripts) { $this->requirementsHtml[$location] .= $this->generateJsElements($scripts); } } } protected function includeCss() { // Filter and check CSS $stylesheets = array_diff_key($this->css,$this->blocked); $customCss = array_diff_key($this->customCSS, $this->blocked); // Stylesheets if(!empty($stylesheets)) { $stylesheets = $this->sortByPriority($stylesheets); // Generate stylesheet links foreach($stylesheets as $files) { $this->requirementsHtml['head'] .= $this->generateCssElements($files); } } // Custom CSS - inserted after stylesheet links if(!empty($customCss)) { foreach($customCss as $styles) { $this->requirementsHtml['head'] .= $this->generateCssElements($styles); } } } protected function includeCustomHeadTags() { // Filter and check tags $tags = array_diff_key($this->customHeadTags,$this->blocked); if(empty($tags)) { return; // None or all blocked } // Make HTML foreach($tags as $tag) { $this->requirementsHtml['head'] .= "$tag\n"; } } /* * ------------------------------------------------------------------------- * Processing methods * ------------------------------------------------------------------------- */ protected function typeAllowed($type) { return in_array((string) $type, array('css','js','javascript')); } /** * @see Requirements_Backend::combile_files() . This method is similar but * allows for providing a mixed list of css/js assets. It separates the css and js * and adds suffixes if necessary. It also takes options for location, priority, media, etc. * @param string $combinedFileName * @param array $files * @param array $options * @return boolean */ public function extendedCombine($combinedFileName, $files, $options = array()) { $filesByType = $this->separateFilesByType($files); // Remove extension (css|js) from file name, because assets may be mixed $combinedFileName = $this->removeFileExtension($combinedFileName); foreach($filesByType as $type => $fileArr) { $combinedName = $this->addFileExtension($combinedFileName,$type); // duplicate check foreach($this->combine_files as $_combinedFileName => $_files) { $duplicates = array_intersect($_files, $fileArr); if($duplicates && $combinedName != $_combinedFileName) { user_error("TkiRequirements_Backend::extendedCombine(): Already included files " . implode(',', $duplicates) . " in combined file '{$_combinedFileName}'", E_USER_NOTICE); return false; } } // Add to javascript/css arrays switch($type) { case 'css': $this->addCss($fileArr,$options); break; case 'js': case 'javascript': $this->addJs($fileArr,$options); break; } $this->combine_files[$combinedName] = $fileArr; } } /** * Require multiple files, with options. Allows mixture of CSS and JS files. * * @param array $files Array of files, given in any of the following formats: * * Non-combined * a) Simple array of file paths. eg. array('file1.css','file2.js') * b) Numeric array of files, with individual files detailed as: * array('path' => 'file1.css', 'type' => 'css'), or * array('file1.css','css') * * Combined * c) Associative array with combine key and nested files array. Nested files * array may use formats specified in a) and b) above. * Examples: * 'bundle' => array('file1.css','file2.js','file3.js') * 'bundle' => array( * array('path' => 'file1.css', 'type' => 'css'), * array('path' => 'file2.js', 'type' => 'js'), * array('path' => 'file3.js', 'type' => 'js') * ) * 'bundle' => array( * array('file1.css','css'), * array('file2.js','js'), * array('file3.js','js'), * ) * All three examples above of combined files would produce the same result: * bundle.css and bundle.js * * @param array $options - Associative array of options applied to the files. * Available options: * priority => (int) - load priority * location => (string) - (head|body) - Applicable to JavaScript * * media => (string) - - Applicable to stylesheets * */ public function requireFiles($files,$options=array()) { // Require files foreach($files as $k => $file) { // Has combine key - process as combined if(is_string($k)) { $this->extendedCombine($k,(array) $file,$options); } // Simply include individual file else { $fileInfo = $this->getFileInfo($file); extract($fileInfo); // $path and $ext $method = 'add'. ucfirst($ext); if(method_exists($this,$method)) { $this->$method($path,$options); } } } } /** * Adds CSS files * @param array $files * @param string $options */ protected function addCss($files,$options) { $files = (array) $files; foreach($files as $file) { $this->css[$file] = $options; } } /** * Adds JavaScript files * @param array $files * @param array $options */ protected function addJs($files,$options) { $files = (array) $files; foreach($files as $file) { $this->javascript[$file] = $options; } } /** * Wrapper method * @param array $files * @param array $options */ protected function addJavascript($files,$options) { $this->addJs($files,$options); } /** * Finds file path and extension. * * @param string|array $file - A simple file path string or array which is * either an associative array [ path => path, type => type ] or numeric [ 0 => path 1 => type ] * @return array - Associative array with path and ext keys. * eg. [ 'path' => 'file1.css', 'ext' => 'css' ] */ protected function getFileInfo($file) { $type = ''; // Array if(is_array($file)) { // Associative array if (isset($file['type']) && in_array($file['type'], array('css', 'javascript', 'js'))) { $type = $file['type']; $path = $file['path']; } // Numeric array elseif (isset($file[1]) && in_array($file[1], array('css', 'javascript', 'js'))) { $type = $file[1]; $path = $file[0]; } // Assume path is the first item else { $path = array_shift($file); } } else { $path = $file; } // Determine type, if missing if(!$type) { if(substr($path, -3) == '.js') { $type = 'js'; } elseif(substr($path, -4) == '.css') { $type = 'css'; } else { $type = ''; } } switch(strtolower($type)) { case 'js': case 'javascript': $ext = 'js'; break; case 'css': $ext = 'css'; break; default: $ext = ''; } return compact('path','ext'); } /** * Separates files according to type: CSS or JavaScript. * * @param array $files - @see TkiRequirements_Backend::requireFiles() formats a) and b) * @return array - Array with css,js for keys */ protected function separateFilesByType($files) { $output = array(); foreach($files as $file) { if(empty($file)) { continue; } $fileInfo = $this->getFileInfo($file); extract($fileInfo); // $path and $ext if(!$ext) { user_error("TkiRequirements_Backend::separateFilesByType(): Couldn't guess file type for file '$path', " . "please specify by passing using an array instead.", E_USER_NOTICE); return $output; } if(!$path) { user_error("TkiRequirements_Backend::separateFilesByType(): No file path found for '$path', " . "please specify by passing using an array instead.", E_USER_NOTICE); return $output; } // Add to output array if(!array_key_exists($ext,$output)) { $output[$ext] = array(); } $output[$ext][] = $path; } return $output; } /** * A revised version of the parent method which retains file prioritisation * @see Requirements::process_combined_files() */ public function process_combined_files() { // The class_exists call prevents us loading SapphireTest.php (slow) just to know that // SapphireTest isn't running :-) if(class_exists('SapphireTest', false)) $runningTest = SapphireTest::is_running_test(); else $runningTest = false; if((Director::isDev() && !$runningTest && !isset($_REQUEST['combine'])) || !$this->combined_files_enabled) { return; } // Make a map of files that could be potentially combined $combinerCheck = array(); foreach($this->combine_files as $combinedFile => $sourceItems) { foreach($sourceItems as $sourceItem) { if(isset($combinerCheck[$sourceItem]) && $combinerCheck[$sourceItem] != $combinedFile){ user_error("Requirements_Backend::process_combined_files - file '$sourceItem' appears in two " . "combined files:" . " '{$combinerCheck[$sourceItem]}' and '$combinedFile'", E_USER_WARNING); } $combinerCheck[$sourceItem] = $combinedFile; } } // Work out the relative URL for the combined files from the base folder $combinedFilesFolder = ($this->getCombinedFilesFolder()) ? ($this->getCombinedFilesFolder() . '/') : ''; // Figure out which ones apply to this request $combinedFiles = array(); $newJSRequirements = array(); $newCSSRequirements = array(); foreach($this->javascript as $file => $options) { if(empty($options)) $options = true; if(isset($combinerCheck[$file])) { $newJSRequirements[$combinedFilesFolder . $combinerCheck[$file]] = $options; $combinedFiles[$combinerCheck[$file]] = true; } else { $newJSRequirements[$file] = $options; } } foreach($this->css as $file => $params) { if(isset($combinerCheck[$file])) { // Inherit the parameters from the last file in the combine set. $newCSSRequirements[$combinedFilesFolder . $combinerCheck[$file]] = $params; $combinedFiles[$combinerCheck[$file]] = true; } else { $newCSSRequirements[$file] = $params; } } // Process the combined files $base = Director::baseFolder() . '/'; foreach(array_diff_key($combinedFiles, $this->blocked) as $combinedFile => $dummy) { $fileList = $this->combine_files[$combinedFile]; $combinedFilePath = $base . $combinedFilesFolder . '/' . $combinedFile; // Make the folder if necessary if(!file_exists(dirname($combinedFilePath))) { Filesystem::makeFolder(dirname($combinedFilePath)); } // If the file isn't writeable, don't even bother trying to make the combined file and return. The // files will be included individually instead. This is a complex test because is_writable fails // if the file doesn't exist yet. if((file_exists($combinedFilePath) && !is_writable($combinedFilePath)) || (!file_exists($combinedFilePath) && !is_writable(dirname($combinedFilePath))) ) { user_error("Requirements_Backend::process_combined_files(): Couldn't create '$combinedFilePath'", E_USER_WARNING); return false; } // Determine if we need to build the combined include if(file_exists($combinedFilePath)) { // file exists, check modification date of every contained file $srcLastMod = 0; foreach($fileList as $file) { if(file_exists($base . $file)) { $srcLastMod = max(filemtime($base . $file), $srcLastMod); } } $refresh = $srcLastMod > filemtime($combinedFilePath); } else { // File doesn't exist, or refresh was explicitly required $refresh = true; } if(!$refresh) continue; $failedToMinify = false; $combinedData = ""; foreach(array_diff($fileList, $this->blocked) as $file) { $fileContent = file_get_contents($base . $file); try{ $fileContent = $this->minifyFile($file, $fileContent); }catch(Exception $e){ $failedToMinify = true; } if ($this->write_header_comment) { // Write a header comment for each file for easier identification and debugging. The semicolon between each file is required for jQuery to be combined properly and protects against unterminated statements. $combinedData .= "/****** FILE: $file *****/\n"; } $combinedData .= $fileContent . "\n"; } $successfulWrite = false; $fh = fopen($combinedFilePath, 'wb'); if($fh) { if(fwrite($fh, $combinedData) == strlen($combinedData)) $successfulWrite = true; fclose($fh); unset($fh); } if($failedToMinify){ // Failed to minify, use unminified files instead. This warning is raised at the end to allow code execution // to complete in case this warning is caught inside a try-catch block. user_error('Failed to minify '.$file.', exception: '.$e->getMessage(), E_USER_WARNING); } // Unsuccessful write - just include the regular JS files, rather than the combined one if(!$successfulWrite) { user_error("Requirements_Backend::process_combined_files(): Couldn't create '$combinedFilePath'", E_USER_WARNING); continue; } } // Note: Alters the original information, which means you can't call this method repeatedly - it will behave // differently on the subsequent calls $this->javascript = $newJSRequirements; $this->css = $newCSSRequirements; } /* * ------------------------------------------------------------------------- * Helper methods * ------------------------------------------------------------------------- */ protected function removeFileExtension($file) { return preg_replace('/(.*)\.(css|js)$/i','$1',$file); } /** * Adds file extension if not present, given type. * @param string $file * @param string $type javascript|js|css * @return string */ protected function addFileExtension($file,$type) { $ext = ''; switch($type) { case 'javascript': case 'js': $ext = '.js'; break; case 'css': $ext = '.css'; break; } // Extension empty or already present if(empty($ext) || $this->endsWith($file,$ext)) { return $file; } // Add extension else { return ($file . $ext); } } /** * Tests if string ends with substring * @todo - Move to utility library * @param string $str * @param string $test * @return boolean */ protected function endsWith($str, $substr) { $strLen = strlen($str); $substrLen = strlen($substr); if ($substrLen > $strLen) return false; return substr_compare($str, $substr, $strLen - $substrLen, $substrLen) === 0; } } |