├── .codecov.yml ├── .editorconfig ├── .scrutinizer.yml ├── LICENSE ├── README.md ├── _config.php ├── _config └── devtoolkit.yml ├── bootstrap.php ├── composer.json ├── css └── buildtask.css ├── phpcs.xml.dist ├── phpunit.xml.dist ├── ressources └── deploy_exclude.txt ├── src ├── AdminBasicAuth.php ├── Benchmark.php ├── BetterDebugView.php ├── BuildTaskTools.php ├── Buttons │ └── FastExportButton.php ├── Extensions │ ├── DevBuildExtension.php │ ├── DevGroupExtension.php │ └── EnvSiteConfigExtension.php ├── Fake │ └── FakeDataProvider.php ├── Helpers │ ├── DevUtils.php │ ├── DuplicateMembersMerger.php │ ├── EmptySpaceFinder.php │ ├── EnvironmentChecker.php │ ├── FileHelper.php │ └── SubsiteHelper.php ├── Tasks │ ├── ClearCacheFolderTask.php │ ├── DisabledMigrationTask.php │ ├── DropInvalidFilesTask.php │ ├── DropUnusedDatabaseObjectsTask.php │ ├── FakeRecordGeneratorTask.php │ ├── PhpInfoTask.php │ ├── PublishAllFiles.php │ ├── RehashImagesTask.php │ ├── RemoveEmptyGroupsTask.php │ ├── RemoveOldPermissionsTask.php │ ├── SitePublisherTask.php │ └── TestCacheSupportTask.php └── TypographyController.php ├── templates └── Layout │ └── Typography.ss └── tests └── DevToolkitTest.php /.codecov.yml: -------------------------------------------------------------------------------- 1 | comment: off 2 | coverage: 3 | status: 4 | project: off 5 | patch: off 6 | 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # For more information about the properties used in this file, 2 | # please see the EditorConfig documentation: 3 | # http://editorconfig.org 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 4 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [{*.yml,package.json}] 14 | indent_size = 2 15 | 16 | # The indent size used in the package.json file cannot be changed: 17 | # https://github.com/npm/npm/pull/3180#issuecomment-16336516 18 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | inherit: true 2 | 3 | build: 4 | image: default-bionic 5 | environment: 6 | php: 8.1.2 7 | nodes: 8 | analysis: 9 | tests: 10 | override: [php-scrutinizer-run] 11 | 12 | checks: 13 | php: 14 | code_rating: true 15 | duplication: true 16 | 17 | filter: 18 | paths: [src/*, tests/*] 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Thomas Portelange 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SilverStripe Devtoolkit 2 | 3 | [![Build Status](https://travis-ci.com/lekoala/silverstripe-devtoolkit.svg?branch=master)](https://travis-ci.com/lekoala/silverstripe-devtoolkit/) 4 | [![scrutinizer](https://scrutinizer-ci.com/g/lekoala/silverstripe-devtoolkit/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/lekoala/silverstripe-devtoolkit/) 5 | [![Code coverage](https://codecov.io/gh/lekoala/silverstripe-devtoolkit/branch/master/graph/badge.svg)](https://codecov.io/gh/lekoala/silverstripe-devtoolkit) 6 | 7 | A common set of dev tools and helpers for Silverstripe 8 | 9 | ## Improved Debug view 10 | 11 | The BetterDebugView class provided clickable links to trace. It is configured by default for VS Code but you can 12 | configure your own ide placeholder with env var IDE_PLACEHOLDER. 13 | 14 | ## Useful tasks 15 | 16 | 17 | 18 | 19 | ## What's included? 20 | 21 | - AdminBasicAuth : simple http basic auth for .env admin without any login/authenticate/member stuff 22 | - Benchmark : simple way to log time to execute code 23 | - BuildTaskTools : a trait to make your task tools easier to work with 24 | - FastExportButton : to make exporting large table easier by executing raw queries 25 | - FakeDataProvider : a lightweight alternative to faker to get some random stuff 26 | 27 | ## Compatibility 28 | 29 | Tested with ^4.3 30 | 31 | ## Maintainer 32 | 33 | LeKoala - thomas@lekoala.be 34 | -------------------------------------------------------------------------------- /_config.php: -------------------------------------------------------------------------------- 1 | debugVariable($val, \SilverStripe\Dev\Debug::caller(), true, $i); 54 | $i++; 55 | } 56 | if ($doExit) { 57 | exit(); 58 | } 59 | } 60 | } 61 | // Add a logger helper 62 | if (!function_exists('l')) { 63 | function l(): void 64 | { 65 | $priority = 100; 66 | $extras = func_get_args(); 67 | $message = array_shift($extras); 68 | if (!is_string($message)) { 69 | $message = json_encode($message); 70 | } 71 | \SilverStripe\Core\Injector\Injector::inst()->get(\Psr\Log\LoggerInterface::class)->log($priority, $message, $extras); 72 | } 73 | } 74 | 75 | // Timezone setting 76 | $SS_TIMEZONE = Environment::getEnv('SS_TIMEZONE'); 77 | if ($SS_TIMEZONE) { 78 | if (!in_array($SS_TIMEZONE, timezone_identifiers_list())) { 79 | throw new Exception("Timezone $SS_TIMEZONE is not valid"); 80 | } 81 | date_default_timezone_set($SS_TIMEZONE); 82 | } 83 | 84 | $SS_SERVERNAME = $_SERVER['SERVER_NAME'] ?? 'localhost'; 85 | if (Director::isDev()) { 86 | error_reporting(-1); 87 | ini_set('display_errors', true); 88 | 89 | // Enable IDEAnnotator 90 | if (in_array(substr($SS_SERVERNAME, strrpos($SS_SERVERNAME, '.') + 1), ['dev', 'local', 'localhost'])) { 91 | \SilverStripe\Core\Config\Config::modify()->set('SilverLeague\IDEAnnotator\DataObjectAnnotator', 'enabled', true); 92 | \SilverStripe\Core\Config\Config::modify()->merge('SilverLeague\IDEAnnotator\DataObjectAnnotator', 'enabled_modules', [ 93 | 'app' 94 | ]); 95 | } 96 | 97 | // Fixes https://github.com/silverleague/silverstripe-ideannotator/issues/122 98 | \SilverStripe\Core\Config\Config::modify()->set('SilverLeague\IDEAnnotator\Tests\Team', 'has_many', []); 99 | } 100 | 101 | // When running tests, use SQLite3 102 | // @link https://docs.silverstripe.org/en/4/developer_guides/testing/ 103 | if (Director::is_cli()) { 104 | if (isset($_SERVER['argv'][0]) && $_SERVER['argv'][0] == 'vendor/bin/phpunit') { 105 | global $databaseConfig; 106 | if (class_exists(\SilverStripe\SQLite\SQLite3Database::class)) { 107 | $databaseConfig['type'] = 'SQLite3Database'; 108 | $databaseConfig['path'] = ':memory:'; 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /_config/devtoolkit.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Name: devtoolkit 3 | Only: 4 | environment: dev 5 | --- 6 | SilverStripe\Control\Director: 7 | rules: 8 | 'typo': 'LeKoala\DevToolkit\TypographyController' 9 | SilverStripe\Dev\DevBuildController: 10 | extensions: 11 | - LeKoala\DevToolkit\Extensions\DevBuildExtension 12 | SilverStripe\Core\Injector\Injector: 13 | SilverStripe\Dev\DebugView: 14 | class: LeKoala\DevToolkit\BetterDebugView 15 | --- 16 | Name: devtoolkit-all 17 | --- 18 | SilverStripe\Core\Injector\Injector: 19 | SilverStripe\Dev\MigrationTask: 20 | class: LeKoala\DevToolkit\Tasks\DisabledMigrationTasks 21 | -------------------------------------------------------------------------------- /bootstrap.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | CodeSniffer ruleset for SilverStripe coding conventions. 4 | 5 | src 6 | tests 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | tests 5 | 6 | 7 | 8 | 9 | src/ 10 | 11 | tests/ 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /ressources/deploy_exclude.txt: -------------------------------------------------------------------------------- 1 | # Syntax help: 2 | # /dir/ means exclude the root folder /dir 3 | # /dir/* means get the root folder /dir but not the contents 4 | # dir/ means exclude any folder anywhere where the name contains dir/ 5 | # fileOrDir means exclude any file or folder called fileOrDir. Use this for folders you don't want to manage but exists on the server 6 | 7 | # never sync assets 8 | /assets 9 | /public/assets 10 | 11 | # never sync _ or . prefixed 12 | /_* 13 | /.* 14 | 15 | # ide and dev elements 16 | /nbproject 17 | /debugbar 18 | 19 | # silverstripe specific files 20 | /silverstripe-cache/ 21 | 22 | # php related 23 | 24 | # silverstripe 4 needs composer.json files to be deployed for manifest to work 25 | # composer.json 26 | composer.lock 27 | phpunit.xml 28 | phpunit.xml.dist 29 | phpcs.xml 30 | phpcs.xml.dist 31 | 32 | # dev and versioning 33 | *.log 34 | *.sh 35 | *.bat 36 | *.md 37 | /deploy*.ini 38 | /deploy_exclude.txt 39 | 40 | # server stuff 41 | /stats 42 | /awstats-icon 43 | /awstatsicons 44 | /icon 45 | -------------------------------------------------------------------------------- /src/AdminBasicAuth.php: -------------------------------------------------------------------------------- 1 | > 13 | */ 14 | protected static array $metrics = []; 15 | 16 | /** 17 | * For example 0.001 18 | * 19 | * @var float 20 | */ 21 | public static float $slow_threshold = 0; 22 | 23 | /** 24 | * @return \Monolog\Logger 25 | */ 26 | public static function getLogger() 27 | { 28 | $parts = explode("\\", get_called_class()); 29 | $class = array_pop($parts); 30 | return Injector::inst()->get(LoggerInterface::class)->withName(ClassHelper::getClassWithoutNamespace($class)); 31 | } 32 | 33 | /** 34 | * A dead simple benchmark function 35 | * 36 | * Usage : bm(function() { // Insert here the code to benchmark }); 37 | * Alternative usage : bm() ; // Code to test ; bm(); 38 | * 39 | * @param callable $cb 40 | * @return void 41 | */ 42 | public static function run($cb = null) 43 | { 44 | $data = self::benchmark($cb); 45 | if (!$data) { 46 | return; 47 | } 48 | printf("It took %s seconds and used %s memory", $data['time'], $data['memory']); 49 | die(); 50 | } 51 | 52 | /** 53 | * @param null|callable $cb 54 | * @param string|null $name 55 | * @return false|array{'time': string, 'memory': string} 56 | */ 57 | protected static function benchmark($cb = null, $name = null) 58 | { 59 | static $_data = null; 60 | 61 | if ($name) { 62 | $data = self::$metrics[$name] ?? null; 63 | } else { 64 | $data = $_data; 65 | } 66 | 67 | // No callback scenario 68 | if ($cb === null) { 69 | if ($data === null) { 70 | $data = [ 71 | 'startTime' => microtime(true), 72 | 'startMemory' => memory_get_usage(), 73 | ]; 74 | if ($name) { 75 | self::$metrics[$name] = $data; 76 | } 77 | // Allow another call 78 | return false; 79 | } else { 80 | $startTime = $data['startTime']; 81 | $startMemory = $data['startMemory']; 82 | 83 | // Clear for future calls 84 | if (!$name) { 85 | $_data = null; 86 | } 87 | } 88 | } else { 89 | $startTime = microtime(true); 90 | $startMemory = memory_get_usage(); 91 | 92 | $cb(); 93 | } 94 | 95 | $endTime = microtime(true); 96 | $endMemory = memory_get_usage(); 97 | 98 | $time = sprintf("%.6f", $endTime - $startTime); 99 | $memory = self::bytesToHuman($endMemory - $startMemory); 100 | 101 | return [ 102 | 'time' => $time, 103 | 'memory' => $memory, 104 | ]; 105 | } 106 | 107 | protected static function bytesToHuman(float $bytes, int $decimals = 2): string 108 | { 109 | if ($bytes == 0) { 110 | return "0.00 B"; 111 | } 112 | $e = floor(log($bytes, 1024)); 113 | return round($bytes / pow(1024, $e), 2) . ['B', 'KB', 'MB', 'GB', 'TB', 'PB'][$e]; 114 | } 115 | 116 | /** 117 | * @param string $name 118 | * @param null|callable $cb 119 | * @return void 120 | */ 121 | public static function log(string $name, $cb = null): void 122 | { 123 | // Helps dealing with nasty ajax calls in the admin 124 | $ignoredPaths = [ 125 | 'schema/' 126 | ]; 127 | $requestUri = $_SERVER['REQUEST_URI'] ?? ''; 128 | foreach ($ignoredPaths as $ignoredPath) { 129 | if (str_contains($requestUri, $ignoredPath)) { 130 | return; 131 | } 132 | } 133 | $data = self::benchmark($cb, $name); 134 | if (!$data) { 135 | return; 136 | } 137 | 138 | $time = $data['time']; 139 | $memory = $data['memory']; 140 | 141 | if (self::$slow_threshold && $time < self::$slow_threshold) { 142 | return; 143 | } 144 | 145 | $bt = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); 146 | $line = $bt[1]['line'] ?? 0; 147 | $file = basename($bt[1]['file'] ?? "unknown"); 148 | 149 | self::getLogger()->debug("$name: $time seconds | $memory memory.", [$requestUri, "$file:$line"]); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/BetterDebugView.php: -------------------------------------------------------------------------------- 1 | ide_placeholder; 39 | 40 | // each dev can define their own settings 41 | $envPlaceholder = Environment::getEnv('IDE_PLACEHOLDER'); 42 | if ($envPlaceholder) { 43 | $placeholder = $envPlaceholder; 44 | } 45 | 46 | $ide_link = str_replace(['{file}', '{line}'], [$file, $line], $placeholder); 47 | $link = "$shortname:$line"; 48 | return $link; 49 | } 50 | 51 | /** 52 | * Similar to renderVariable() but respects debug() method on object if available 53 | * 54 | * @param mixed $val 55 | * @param array $caller 56 | * @param bool $showHeader 57 | * @param int|null $argumentIndex 58 | * @return string 59 | */ 60 | public function debugVariable($val, $caller, $showHeader = true, $argumentIndex = 0) 61 | { 62 | // Get arguments name 63 | $args = $this->extractArgumentsName($caller['file'], $caller['line']); 64 | 65 | if ($showHeader) { 66 | $callerFormatted = $this->formatCaller($caller); 67 | $defaultArgumentName = is_int($argumentIndex) ? 'Debug' : $argumentIndex; 68 | $argumentName = $args[$argumentIndex] ?? $defaultArgumentName; 69 | 70 | // Sql trick 71 | if (strpos(strtolower($argumentName), 'sql') !== false && is_string($val)) { 72 | $text = DatabaseHelper::formatSQL($val); 73 | } else { 74 | $text = $this->debugVariableText($val); 75 | } 76 | 77 | $html = "
\n
\n" 78 | . "

$argumentName ($callerFormatted)\n

\n" 79 | . $text 80 | . "
"; 81 | 82 | if (Director::is_ajax()) { 83 | $html = strip_tags($html); 84 | } 85 | 86 | return $html; 87 | } 88 | return $this->debugVariableText($val); 89 | } 90 | 91 | /** 92 | * @param string $file 93 | * @param int $line 94 | * @return array 95 | */ 96 | protected function extractArgumentsName($file, $line) 97 | { 98 | // Arguments passed to the function are stored in matches 99 | $src = file($file); 100 | $src_line = $src[$line - 1]; 101 | preg_match("/d\((.+)\)/", $src_line, $matches); 102 | // Find all arguments, ignore variables within parenthesis 103 | $arguments = array(); 104 | if (!empty($matches[1])) { 105 | $arguments = array_map('trim', preg_split("/(?![^(]*\)),/", $matches[1])); 106 | } 107 | return $arguments; 108 | } 109 | 110 | /** 111 | * Get debug text for this object 112 | * 113 | * Use symfony dumper if it exists 114 | * 115 | * @param mixed $val 116 | * @return string 117 | */ 118 | public function debugVariableText($val) 119 | { 120 | // Empty stuff is tricky 121 | if (empty($val)) { 122 | $valtype = gettype($val); 123 | return "(empty $valtype)"; 124 | } 125 | 126 | if (Director::is_ajax()) { 127 | // In ajax context, we can still use debug info 128 | if (is_object($val)) { 129 | if (ClassInfo::hasMethod($val, 'debug')) { 130 | return $val->debug(); 131 | } 132 | return print_r($val, true); 133 | } 134 | } else { 135 | // Otherwise, we'd rater a full and usable object dump 136 | if (function_exists('dump') && (is_object($val) || is_array($val))) { 137 | ob_start(); 138 | dump($val); 139 | return ob_get_clean(); 140 | } 141 | } 142 | 143 | // Check debug 144 | if (is_object($val) && ClassInfo::hasMethod($val, 'debug')) { 145 | return $val->debug(); 146 | } 147 | 148 | // Format as array 149 | if (is_array($val)) { 150 | return print_r($val, true); 151 | } 152 | 153 | // Format object 154 | if (is_object($val)) { 155 | return var_export($val, true); 156 | } 157 | 158 | // Format bool 159 | if (is_bool($val)) { 160 | return '(bool) ' . ($val ? 'true' : 'false'); 161 | } 162 | 163 | // Format text 164 | $html = Convert::raw2xml($val); 165 | return "
{$html}
\n"; 166 | } 167 | 168 | /** 169 | * Formats the caller of a method 170 | * 171 | * Improve method by creating the ide link 172 | * 173 | * @param array $caller 174 | * @return string 175 | */ 176 | protected function formatCaller($caller) 177 | { 178 | $return = self::makeIdeLink($caller['file'], $caller['line']); 179 | if (!empty($caller['class']) && !empty($caller['function'])) { 180 | $return .= " - {$caller['class']}::{$caller['function']}()"; 181 | } 182 | return $return; 183 | } 184 | 185 | /** 186 | * Render an error. 187 | * 188 | * @param string $httpRequest the kind of request 189 | * @param int $errno Codenumber of the error 190 | * @param string $errstr The error message 191 | * @param string $errfile The name of the soruce code file where the error occurred 192 | * @param int $errline The line number on which the error occured 193 | * @return string 194 | */ 195 | public function renderError($httpRequest, $errno, $errstr, $errfile, $errline) 196 | { 197 | $errorType = isset(self::$error_types[$errno]) ? self::$error_types[$errno] : self::$unknown_error; 198 | $httpRequestEnt = htmlentities($httpRequest, ENT_COMPAT, 'UTF-8'); 199 | if (ini_get('html_errors')) { 200 | $errstr = strip_tags($errstr); 201 | } else { 202 | $errstr = Convert::raw2xml($errstr); 203 | } 204 | 205 | $infos = self::makeIdeLink($errfile, $errline); 206 | 207 | $output = '
'; 208 | $output .= "

[" . $errorType['title'] . '] ' . $errstr . "

"; 209 | $output .= "

$httpRequestEnt

"; 210 | $output .= "

$infos

"; 211 | $output .= '
'; 212 | 213 | return $output; 214 | } 215 | 216 | /** 217 | * @param Exception $exception 218 | * @return void 219 | */ 220 | public function writeException(Exception $exception) 221 | { 222 | $infos = self::makeIdeLink($exception->getFile(), $exception->getLine()); 223 | 224 | $output = '
'; 225 | $output .= "

" . get_class($exception) . " in $infos

"; 226 | $message = $exception->getMessage(); 227 | if ($exception instanceof DatabaseException) { 228 | $sql = $exception->getSQL(); 229 | // Some database errors don't have sql 230 | if ($sql) { 231 | $parameters = $exception->getParameters(); 232 | $sql = DB::inline_parameters($sql, $parameters); 233 | $formattedSQL = DatabaseHelper::formatSQL($sql); 234 | $message .= "

Couldn't run query:
" . $formattedSQL; 235 | } 236 | } 237 | $output .= "

" . $message . "

"; 238 | $output .= '
'; 239 | 240 | $output .= $this->renderTrace($exception->getTrace()); 241 | 242 | echo $output; 243 | } 244 | 245 | /** 246 | * Render a call track 247 | * 248 | * @param array $trace The debug_backtrace() array 249 | * @return string 250 | */ 251 | public function renderTrace($trace) 252 | { 253 | $output = '
'; 254 | $output .= '

Trace

'; 255 | $output .= self::get_rendered_backtrace($trace); 256 | $output .= '
'; 257 | 258 | return $output; 259 | } 260 | 261 | /** 262 | * Render a backtrace array into an appropriate plain-text or HTML string. 263 | * 264 | * @param array $bt The trace array, as returned by debug_backtrace() or Exception::getTrace() 265 | * @return string The rendered backtrace 266 | */ 267 | public static function get_rendered_backtrace($bt) 268 | { 269 | if (empty($bt)) { 270 | return ''; 271 | } 272 | $result = '
    '; 273 | foreach ($bt as $item) { 274 | if ($item['function'] == 'user_error') { 275 | $name = $item['args'][0]; 276 | } else { 277 | $name = Backtrace::full_func_name($item, true); 278 | } 279 | $result .= "
  • " . htmlentities($name, ENT_COMPAT, 'UTF-8') . "\n
    \n"; 280 | if (!isset($item['file']) || !isset($item['line'])) { 281 | $result .= isset($item['file']) ? htmlentities(basename($item['file']), ENT_COMPAT, 'UTF-8') : ''; 282 | $result .= isset($item['line']) ? ":$item[line]" : ''; 283 | } else { 284 | $result .= self::makeIdeLink($item['file'], $item['line']); 285 | } 286 | $result .= "
  • \n"; 287 | } 288 | $result .= '
'; 289 | return $result; 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /src/BuildTaskTools.php: -------------------------------------------------------------------------------- 1 | message("Time limit is disabled", "info"); 39 | } else { 40 | $this->message("Time limit has been set to $timeLimit seconds", "info"); 41 | } 42 | } 43 | 44 | /** 45 | * Rebuild the class manifest 46 | * 47 | * @return void 48 | */ 49 | protected function regenerateClassManifest() 50 | { 51 | ClassLoader::inst()->getManifest()->regenerate(false); 52 | $this->message("The class manifest has been rebuilt", "created"); 53 | } 54 | 55 | /** 56 | * All dataobjects 57 | * 58 | * @return array 59 | */ 60 | protected function getValidDataObjects() 61 | { 62 | $list = ClassInfo::getValidSubClasses(DataObject::class); 63 | array_shift($list); 64 | return $list; 65 | } 66 | 67 | /** 68 | * All modules 69 | * 70 | * @return array 71 | */ 72 | protected function getModules() 73 | { 74 | return ArrayLib::valuekey(array_keys(ModuleLoader::inst()->getManifest()->getModules())); 75 | } 76 | 77 | /** 78 | * Get the request (and keep your imports clean :-) ) 79 | * 80 | * @return HTTPRequest 81 | */ 82 | protected function getRequest() 83 | { 84 | if (!$this->request) { 85 | die('Make sure to call $this->request = $request in your own class'); 86 | } 87 | return $this->request; 88 | } 89 | 90 | /** 91 | * Add options (to be called later with askOptions) 92 | * 93 | * @param string $key 94 | * @param string $title 95 | * @param mixed $default Default value. Input type will be based on this (bool => checkbox, etc) 96 | * @param array|Map $list An array of value for a dropdown 97 | * @return void 98 | */ 99 | protected function addOption($key, $title, $default = '', $list = null) 100 | { 101 | // Handle maps 102 | if (is_object($list) && method_exists($list, 'toArray')) { 103 | $list = $list->toArray(); 104 | } 105 | $opt = [ 106 | 'key' => $key, 107 | 'title' => $title, 108 | 'default' => $default, 109 | 'list' => $list, 110 | ]; 111 | $this->options[] = $opt; 112 | 113 | return $opt; 114 | } 115 | 116 | /** 117 | * Display a form with options 118 | * 119 | * Options are added through addOption method 120 | * 121 | * @return array Array with key => value corresponding to options asked 122 | */ 123 | protected function askOptions() 124 | { 125 | $values = []; 126 | $request = $this->getRequest(); 127 | echo '
'; 128 | foreach ($this->options as $opt) { 129 | $val = $request->requestVar($opt['key']); 130 | if ($val === null) { 131 | $val = $opt['default']; 132 | } 133 | 134 | $values[$opt['key']] = $val; 135 | 136 | if ($opt['list']) { 137 | $input = ''; 147 | } else { 148 | $type = 'text'; 149 | $input = null; 150 | if (isset($opt['default'])) { 151 | if (is_bool($opt['default'])) { 152 | $type = 'checkbox'; 153 | $checked = $val ? ' checked="checked"' : ''; 154 | $input = ''; 155 | $input .= ''; 156 | } else { 157 | if (is_int($opt['default'])) { 158 | $type = 'numeric'; 159 | } 160 | } 161 | } 162 | if (!$input) { 163 | $input = ''; 164 | } 165 | } 166 | echo '
'; 167 | echo ''; 168 | echo '
'; 169 | echo '
'; 170 | } 171 | echo '

'; 172 | echo '
'; 173 | echo '
'; 174 | return $values; 175 | } 176 | 177 | protected function message($message, $type = 'default') 178 | { 179 | if (Director::is_cli()) { 180 | $cli_map = [ 181 | 'repaired' => '>', 182 | 'success' => '✓', 183 | 'created' => '+', 184 | 'changed' => '+', 185 | 'bad' => '-', 186 | 'obsolete' => '-', 187 | 'deleted' => '-', 188 | 'notice' => '!', 189 | 'error' => '-', 190 | ]; 191 | 192 | $message = strip_tags($message); 193 | if (isset($cli_map[$type])) { 194 | $message = $cli_map[$type] . ' ' . $message; 195 | } 196 | if (!is_string($message)) { 197 | $message = json_encode($message); 198 | } 199 | echo " $message\n"; 200 | } else { 201 | $web_map = [ 202 | 'info' => 'blue', 203 | 'repaired' => 'blue', 204 | 'success' => 'green', 205 | 'created' => 'green', 206 | 'changed' => 'green', 207 | 'obsolete' => 'red', 208 | 'notice' => 'orange', 209 | 'deleted' => 'red', 210 | 'bad' => 'red', 211 | 'error' => 'red', 212 | ]; 213 | $color = '#000000'; 214 | if (isset($web_map[$type])) { 215 | $color = $web_map[$type]; 216 | } 217 | if (!is_string($message)) { 218 | $message = print_r($message, true); 219 | echo "
$message
"; 220 | } else { 221 | echo "
$message
"; 222 | } 223 | } 224 | } 225 | 226 | protected function isDev() 227 | { 228 | return Director::isDev(); 229 | } 230 | 231 | protected function isLive() 232 | { 233 | return Director::isLive(); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/Buttons/FastExportButton.php: -------------------------------------------------------------------------------- 1 | targetFragment = $targetFragment; 87 | $this->exportColumns = $exportColumns; 88 | self::$instances++; 89 | $this->instance = self::$instances; 90 | } 91 | 92 | public function getActionName() 93 | { 94 | return 'fastexport_' . $this->instance; 95 | } 96 | 97 | /** 98 | * Place the export button in a

tag below the field 99 | */ 100 | public function getHTMLFragments($gridField) 101 | { 102 | $title = $this->buttonTitle ? $this->buttonTitle : _t( 103 | 'TableListField.FASTEXPORT', 104 | 'Fast Export' 105 | ); 106 | 107 | $name = $this->getActionName(); 108 | 109 | $button = new GridField_FormAction( 110 | $gridField, 111 | $name, 112 | $title, 113 | $name, 114 | null 115 | ); 116 | $button->addExtraClass('no-ajax action_export'); 117 | $button->setForm($gridField->getForm()); 118 | 119 | return array( 120 | $this->targetFragment => '

' . $button->Field() . '

', 121 | ); 122 | } 123 | 124 | /** 125 | * export is an action button 126 | */ 127 | public function getActions($gridField) 128 | { 129 | return array($this->getActionName()); 130 | } 131 | 132 | public function handleAction( 133 | GridField $gridField, 134 | $actionName, 135 | $arguments, 136 | $data 137 | ) { 138 | if (in_array($actionName, $this->getActions($gridField))) { 139 | return $this->handleExport($gridField); 140 | } 141 | } 142 | 143 | /** 144 | * it is also a URL 145 | */ 146 | public function getURLHandlers($gridField) 147 | { 148 | return array($this->getActionName() => 'handleExport'); 149 | } 150 | 151 | /** 152 | * Handle the export, for both the action button and the URL 153 | */ 154 | public function handleExport($gridField, $request = null) 155 | { 156 | $now = Date("Ymd_Hi"); 157 | 158 | if ($fileData = $this->generateExportFileData($gridField)) { 159 | $name = $this->exportName; 160 | $ext = 'csv'; 161 | $fileName = "$name-$now.$ext"; 162 | 163 | return HTTPRequest::send_file($fileData, $fileName, 'text/csv'); 164 | } 165 | } 166 | 167 | public static function allFieldsForClass($class) 168 | { 169 | $dataClasses = ClassInfo::dataClassesFor($class); 170 | $fields = array(); 171 | foreach ($dataClasses as $dataClass) { 172 | $databaseFields = DataObject::getSchema()->databaseFields($dataClass); 173 | 174 | $dataFields = []; 175 | foreach ($databaseFields as $name => $type) { 176 | if ($type == 'Text' || $type == 'HTMLText') { 177 | continue; 178 | } 179 | $dataFields[] = $name; 180 | } 181 | $fields = array_merge( 182 | $fields, 183 | $dataFields 184 | ); 185 | } 186 | return array_combine($fields, $fields); 187 | } 188 | 189 | public static function exportFieldsForClass($class) 190 | { 191 | $singl = singleton($class); 192 | if ($singl->hasMethod('exportedFields')) { 193 | return $singl->exportedFields(); 194 | } 195 | $exportedFields = Config::inst()->get($class, 'exported_fields'); 196 | if (!$exportedFields) { 197 | $exportedFields = array_keys(self::allFieldsForClass($class)); 198 | } 199 | $unexportedFields = Config::inst()->get($class, 'unexported_fields'); 200 | if ($unexportedFields) { 201 | $exportedFields = array_diff($exportedFields, $unexportedFields); 202 | } 203 | return array_combine($exportedFields, $exportedFields); 204 | } 205 | 206 | /** 207 | * Generate export fields 208 | * 209 | * @param GridField $gridField 210 | * @return string 211 | */ 212 | public function generateExportFileData($gridField) 213 | { 214 | $class = $gridField->getModelClass(); 215 | $columns = ($this->exportColumns) ? $this->exportColumns : self::exportFieldsForClass($class); 216 | $fileData = ''; 217 | 218 | // If we don't have an associative array 219 | if (!ArrayLib::is_associative($columns)) { 220 | array_combine($columns, $columns); 221 | } 222 | 223 | $singl = singleton($class); 224 | 225 | $singular = $class ? $singl->i18n_singular_name() : ''; 226 | $plural = $class ? $singl->i18n_plural_name() : ''; 227 | 228 | $filter = new FileNameFilter; 229 | if ($this->exportName) { 230 | $this->exportName = $filter->filter($this->exportName); 231 | } else { 232 | $this->exportName = $filter->filter('fastexport-' . $plural); 233 | } 234 | 235 | $fileData = ''; 236 | $separator = $this->csvSeparator; 237 | 238 | $class = $gridField->getModelClass(); 239 | $singl = singleton($class); 240 | $baseTable = $singl->baseTable(); 241 | 242 | $stream = fopen('data://text/plain,' . "", 'w+'); 243 | 244 | // Filter columns 245 | $sqlFields = []; 246 | $baseFields = ['ID', 'Created', 'LastEdited']; 247 | 248 | $joins = []; 249 | $isSubsite = false; 250 | $map = []; 251 | if ($singl->hasMethod('fastExportMap')) { 252 | $map = $singl->fastExportMap(); 253 | } 254 | foreach ($columns as $columnSource => $columnHeader) { 255 | // Allow mapping methods to plain fields 256 | if ($map && isset($map[$columnSource])) { 257 | $columnSource = $map[$columnSource]; 258 | } 259 | if ($columnSource == 'SubsiteID') { 260 | $isSubsite = true; 261 | } 262 | if (in_array($columnSource, $baseFields)) { 263 | $sqlFields[] = $baseTable . '.' . $columnSource; 264 | continue; 265 | } 266 | // Naive join support 267 | if (strpos($columnSource, '.') !== false) { 268 | $parts = explode('.', $columnSource); 269 | 270 | $joinSingl = singleton($parts[0]); 271 | $joinBaseTable = $joinSingl->baseTable(); 272 | 273 | if (!isset($joins[$joinBaseTable])) { 274 | $joins[$joinBaseTable] = []; 275 | } 276 | $joins[$joinBaseTable][] = $parts[1]; 277 | 278 | $sqlFields[] = $joinBaseTable . '.' . $parts[1]; 279 | continue; 280 | } 281 | $fieldTable = ClassInfo::table_for_object_field($class, $columnSource); 282 | if ($fieldTable != $baseTable || !$fieldTable) { 283 | unset($columns[$columnSource]); 284 | } else { 285 | $sqlFields[] = $fieldTable . '.' . $columnSource; 286 | } 287 | } 288 | 289 | if ($this->hasHeader) { 290 | $headers = array(); 291 | 292 | // determine the headers. If a field is callable (e.g. anonymous function) then use the 293 | // source name as the header instead 294 | foreach ($columns as $columnSource => $columnHeader) { 295 | $headers[] = (!is_string($columnHeader) && is_callable($columnHeader)) 296 | ? $columnSource : $columnHeader; 297 | } 298 | 299 | $row = array_values($headers); 300 | // fputcsv($stream, $row, $separator); 301 | 302 | // force quotes 303 | fputs($stream, implode(",", array_map("self::encodeFunc", $row)) . "\n"); 304 | } 305 | 306 | if (empty($sqlFields)) { 307 | $sqlFields = ['ID', 'Created', 'LastEdited']; 308 | } 309 | 310 | $where = []; 311 | $sql = 'SELECT ' . implode(',', $sqlFields) . ' FROM ' . $baseTable; 312 | foreach ($joins as $joinTable => $joinFields) { 313 | $foreignKey = $joinTable . 'ID'; 314 | $sql .= ' LEFT JOIN ' . $joinTable . ' ON ' . $joinTable . '.ID = ' . $baseTable . '.' . $foreignKey; 315 | } 316 | // Basic subsite support 317 | if ($isSubsite && class_exists('Subsite') && Subsite::currentSubsiteID()) { 318 | $where[] = $baseTable . '.SubsiteID = ' . Subsite::currentSubsiteID(); 319 | } 320 | 321 | $singl->extend('updateFastExport', $sql, $where); 322 | 323 | // Basic where clause 324 | if (!empty($where)) { 325 | $sql .= ' WHERE ' . implode(' AND ', $where); 326 | } 327 | 328 | $query = DB::query($sql); 329 | 330 | foreach ($query as $row) { 331 | // fputcsv($stream, $row, $separator); 332 | 333 | // force quotes 334 | fputs($stream, implode(",", array_map("self::encodeFunc", $row)) . "\n"); 335 | } 336 | 337 | rewind($stream); 338 | $fileData = stream_get_contents($stream); 339 | fclose($stream); 340 | 341 | return $fileData; 342 | } 343 | 344 | public static function encodeFunc($value) 345 | { 346 | ///remove any ESCAPED double quotes within string. 347 | $value = str_replace('\\"', '"', $value); 348 | //then force escape these same double quotes And Any UNESCAPED Ones. 349 | $value = str_replace('"', '\"', $value); 350 | //force wrap value in quotes and return 351 | return '"' . $value . '"'; 352 | } 353 | 354 | /** 355 | * @return array 356 | */ 357 | public function getExportColumns() 358 | { 359 | return $this->exportColumns; 360 | } 361 | 362 | /** 363 | * @param array 364 | */ 365 | public function setExportColumns($cols) 366 | { 367 | $this->exportColumns = $cols; 368 | return $this; 369 | } 370 | 371 | /** 372 | * @return boolean 373 | */ 374 | public function getHasHeader() 375 | { 376 | return $this->hasHeader; 377 | } 378 | 379 | /** 380 | * @param boolean 381 | */ 382 | public function setHasHeader($bool) 383 | { 384 | $this->hasHeader = $bool; 385 | return $this; 386 | } 387 | 388 | /** 389 | * @return string 390 | */ 391 | public function getExportName() 392 | { 393 | return $this->exportName; 394 | } 395 | 396 | /** 397 | * @param string $exportName 398 | * @return $this 399 | */ 400 | public function setExportName($exportName) 401 | { 402 | $this->exportName = $exportName; 403 | return $this; 404 | } 405 | 406 | /** 407 | * @return string 408 | */ 409 | public function getButtonTitle() 410 | { 411 | return $this->buttonTitle; 412 | } 413 | 414 | /** 415 | * @param string $buttonTitle 416 | * @return $this 417 | */ 418 | public function setButtonTitle($buttonTitle) 419 | { 420 | $this->buttonTitle = $buttonTitle; 421 | return $this; 422 | } 423 | 424 | /** 425 | * @return string 426 | */ 427 | public function getCsvSeparator() 428 | { 429 | return $this->csvSeparator; 430 | } 431 | 432 | /** 433 | * @param string 434 | */ 435 | public function setCsvSeparator($separator) 436 | { 437 | $this->csvSeparator = $separator; 438 | return $this; 439 | } 440 | } 441 | -------------------------------------------------------------------------------- /src/Extensions/DevBuildExtension.php: -------------------------------------------------------------------------------- 1 | owner; 46 | } 47 | 48 | /** 49 | * @return HTTPRequest 50 | */ 51 | public function getRequest() 52 | { 53 | return $this->getExtensionOwner()->getRequest(); 54 | } 55 | 56 | /** 57 | * @return void 58 | */ 59 | public function beforeCallActionHandler() 60 | { 61 | $this->currentSubsite = SubsiteHelper::currentSubsiteID(); 62 | 63 | $annotate = $this->getRequest()->getVar('annotate'); 64 | if ($annotate) { 65 | \SilverLeague\IDEAnnotator\DataObjectAnnotator::config()->enabled = true; 66 | } else { 67 | \SilverLeague\IDEAnnotator\DataObjectAnnotator::config()->enabled = false; 68 | } 69 | 70 | $renameColumns = $this->getRequest()->getVar('fixTableCase'); 71 | if ($renameColumns) { 72 | $this->displayMessage("

Fixing tables case

    \n\n"); 73 | $this->fixTableCase(); 74 | $this->displayMessage("
\n

Tables fixed!

"); 75 | } 76 | 77 | $renameColumns = $this->getRequest()->getVar('renameColumns'); 78 | if ($renameColumns) { 79 | $this->displayMessage("

Renaming columns

    \n\n"); 80 | $this->renameColumns(); 81 | $this->displayMessage("
\n

Columns renamed!

"); 82 | } 83 | 84 | $truncateSiteTree = $this->getRequest()->getVar('truncateSiteTree'); 85 | if ($truncateSiteTree) { 86 | $this->displayMessage("

Truncating SiteTree

    \n\n"); 87 | $this->truncateSiteTree(); 88 | $this->displayMessage("
\n

SiteTree truncated!

"); 89 | } 90 | 91 | // Reverse the logic, don't populate by default 92 | DevUtils::updatePropCb($this->getRequest(), 'getVars', function ($arr) { 93 | if ($this->getRequest()->getVar('populate')) { 94 | $arr['dont_populate'] = false; 95 | } 96 | return $arr; 97 | }); 98 | } 99 | 100 | protected function fixTableCase(): void 101 | { 102 | if (!Director::isDev()) { 103 | throw new Exception("Only available in dev mode"); 104 | } 105 | 106 | $conn = DB::get_conn(); 107 | $dbName = $conn->getSelectedDatabase(); 108 | 109 | $tablesSql = "SELECT table_name FROM information_schema.tables WHERE table_schema = '$dbName';"; 110 | 111 | $result = DB::query($tablesSql); 112 | 113 | //TODO: check list of tables name and match any lowercased one to the right one from the db schema 114 | } 115 | 116 | protected function truncateSiteTree(): void 117 | { 118 | if (!Director::isDev()) { 119 | throw new Exception("Only available in dev mode"); 120 | } 121 | 122 | $sql = <<displayMessage($sql); 137 | } 138 | 139 | /** 140 | * Loop on all DataObjects and look for rename_columns property 141 | * 142 | * It will rename old columns from old_value => new_value 143 | */ 144 | protected function renameColumns(): void 145 | { 146 | $classes = $this->getDataObjects(); 147 | 148 | foreach ($classes as $class) { 149 | if (!property_exists($class, 'rename_columns')) { 150 | continue; 151 | } 152 | 153 | $fields = $class::$rename_columns; 154 | 155 | $schema = DataObject::getSchema(); 156 | $tableName = $schema->baseDataTable($class); 157 | 158 | $dbSchema = DB::get_schema(); 159 | foreach ($fields as $oldName => $newName) { 160 | if ($dbSchema->hasField($tableName, $oldName)) { 161 | if ($dbSchema->hasField($tableName, $newName)) { 162 | $this->displayMessage("
  • $oldName still exists in $tableName. Data will be migrated to $newName and old column $oldName will be dropped.
  • "); 163 | // Migrate data 164 | DB::query("UPDATE $tableName SET $newName = $oldName WHERE $newName IS NULL"); 165 | // Remove column 166 | DB::query("ALTER TABLE $tableName DROP COLUMN $oldName"); 167 | } else { 168 | $this->displayMessage("
  • Renaming $oldName to $newName in $tableName
  • "); 169 | $dbSchema->renameField($tableName, $oldName, $newName); 170 | } 171 | } else { 172 | $this->displayMessage("
  • $oldName does not exist anymore in $tableName
  • "); 173 | } 174 | 175 | // Look for fluent 176 | $fluentTable = $tableName . '_Localised'; 177 | if ($dbSchema->hasTable($fluentTable)) { 178 | if ($dbSchema->hasField($fluentTable, $oldName)) { 179 | if ($dbSchema->hasField($fluentTable, $newName)) { 180 | $this->displayMessage("
  • $oldName still exists in $fluentTable. Data will be migrated to $newName and old column $oldName will be dropped.
  • "); 181 | // Migrate data 182 | DB::query("UPDATE $fluentTable SET $newName = $oldName WHERE $newName IS NULL"); 183 | // Remove column 184 | DB::query("ALTER TABLE $fluentTable DROP COLUMN $oldName"); 185 | } else { 186 | $this->displayMessage("
  • Renaming $oldName to $newName in $fluentTable
  • "); 187 | $dbSchema->renameField($fluentTable, $oldName, $newName); 188 | } 189 | } else { 190 | $this->displayMessage("
  • $oldName does not exist anymore in $fluentTable
  • "); 191 | } 192 | } 193 | } 194 | } 195 | } 196 | 197 | public function afterCallActionHandler(): void 198 | { 199 | // Other helpers 200 | $clearCache = $this->owner->getRequest()->getVar('clearCache'); 201 | $clearEmptyFolders = $this->owner->getRequest()->getVar('clearEmptyFolders'); 202 | 203 | $this->displayMessage("
    "); 204 | if ($clearCache) { 205 | $this->clearCache(); 206 | } 207 | if ($clearEmptyFolders) { 208 | $this->clearEmptyFolders(); 209 | } 210 | $this->displayMessage("
    "); 211 | 212 | // Restore subsite 213 | if ($this->currentSubsite) { 214 | SubsiteHelper::changeSubsite($this->currentSubsite); 215 | } 216 | 217 | $provisionLocales = $this->owner->getRequest()->getVar('provisionLocales'); 218 | if ($provisionLocales) { 219 | $this->displayMessage("

    Provisioning locales

      \n\n"); 220 | try { 221 | \LeKoala\Multilingual\LangHelper::provisionLocales(); 222 | $this->displayMessage("
    \n

    Locales provisioned!

    "); 223 | } catch (Exception $ex) { 224 | $this->displayMessage($ex->getMessage() . '
    '); 225 | } 226 | } 227 | } 228 | 229 | protected function clearCache(): void 230 | { 231 | $this->displayMessage("Clearing cache folder"); 232 | $folder = Director::baseFolder() . '/silverstripe-cache'; 233 | if (!is_dir($folder)) { 234 | $this->displayMessage("silverstripe-cache does not exist in base folder\n"); 235 | return; 236 | } 237 | FileHelper::rmDir($folder); 238 | mkdir($folder, 0755); 239 | $this->displayMessage("Cleared silverstripe-cache folder\n"); 240 | } 241 | 242 | protected function clearEmptyFolders(): void 243 | { 244 | $this->displayMessage("Clearing empty folders in assets"); 245 | $folder = Director::publicFolder() . '/assets'; 246 | if (!is_dir($folder)) { 247 | $this->displayMessage("assets folder does not exist in public folder\n"); 248 | return; 249 | } 250 | 251 | $objects = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($folder), RecursiveIteratorIterator::SELF_FIRST); 252 | foreach ($objects as $name => $object) { 253 | if ($object->isDir()) { 254 | $path = $object->getPath(); 255 | if (!is_readable($path)) { 256 | $this->displayMessage("$path is not readable\n"); 257 | continue; 258 | } 259 | if (!FileHelper::dirContainsChildren($path)) { 260 | rmdir($path); 261 | $this->displayMessage("Removed $path\n"); 262 | } 263 | } 264 | } 265 | } 266 | 267 | /** 268 | * @return array 269 | */ 270 | protected function getDataObjects() 271 | { 272 | $classes = ClassInfo::subclassesFor(DataObject::class); 273 | array_shift($classes); // remove dataobject 274 | return $classes; 275 | } 276 | 277 | /** 278 | * @param string $message 279 | */ 280 | protected function displayMessage($message): void 281 | { 282 | echo Director::is_cli() ? strip_tags($message) : nl2br($message); 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /src/Extensions/DevGroupExtension.php: -------------------------------------------------------------------------------- 1 | owner->Members()->count() == 0) { 28 | $groupTitle = $this->owner->Title; 29 | $filter = new URLSegmentFilter; 30 | 31 | $service = DefaultAdminService::singleton(); 32 | $defaultAdmin = $service->findOrCreateDefaultAdmin(); 33 | $emailParts = explode('@', $defaultAdmin->Email); 34 | 35 | // Let's create a fake member for this 36 | $member = Member::create(); 37 | $member->Email = $filter->filter($groupTitle) . '@' . $emailParts[1]; 38 | $member->FirstName = 'Default User'; 39 | $member->Surname = $groupTitle; 40 | $member->write(); 41 | 42 | $member->changePassword('Test0000'); 43 | $member->write(); 44 | 45 | $this->owner->Members()->add($member); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Extensions/EnvSiteConfigExtension.php: -------------------------------------------------------------------------------- 1 | addFieldToTab( 25 | 'Root.Env', 26 | $field = $class::create( 27 | 'SS_Environment', 28 | null, 29 | file_get_contents($SS_ENVIRONMENT_FILE) 30 | ) 31 | ); 32 | if (!is_writable($SS_ENVIRONMENT_FILE)) { 33 | $field->setReadonly(true); 34 | } 35 | } 36 | } 37 | 38 | public function setSS_Environment($v) 39 | { 40 | $SS_ENVIRONMENT_FILE = Director::baseFolder() . '/.env'; 41 | if (!is_writable($SS_ENVIRONMENT_FILE)) { 42 | throw new Exception('Environment file must be writable'); 43 | } else { 44 | return file_put_contents($SS_ENVIRONMENT_FILE, $v); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Fake/FakeDataProvider.php: -------------------------------------------------------------------------------- 1 | '4880 Glory Rd', 31 | 'City' => 'Ponchatoula', 32 | 'Postcode' => 'LA 70454', 33 | 'Country' => 'US' 34 | ], 35 | [ 36 | 'Address' => '4363 Willow Oaks Lane', 37 | 'City' => 'Harrison Township', 38 | 'Postcode' => 'NJ 08062', 39 | 'Country' => 'US' 40 | ], 41 | [ 42 | 'Address' => '3471 Chipmunk Ln', 43 | 'City' => 'Clifton Heights', 44 | 'Postcode' => 'PA 19018 ‎', 45 | 'Country' => 'US' 46 | ], 47 | [ 48 | 'Address' => '666 Koala Ln', 49 | 'City' => 'Mt Laurel', 50 | 'Postcode' => 'NJ 08054‎', 51 | 'Country' => 'US' 52 | ], 53 | [ 54 | 'Address' => '3339 Little Acres Ln', 55 | 'City' => 'Woodford', 56 | 'Postcode' => 'VA 22580', 57 | 'Country' => 'US' 58 | ], 59 | [ 60 | 'Address' => '15 Anthony Avenue', 61 | 'City' => 'Essex', 62 | 'Postcode' => 'MD 21221', 63 | 'Country' => 'US' 64 | ], 65 | [ 66 | 'Address' => '2942 Kelly Ave', 67 | 'City' => 'Baltimore', 68 | 'Postcode' => 'MD 21209', 69 | 'Country' => 'US' 70 | ], 71 | [ 72 | 'Address' => '687 Burke Rd', 73 | 'City' => 'Delta', 74 | 'Postcode' => 'PA 17314', 75 | 'Country' => 'US' 76 | ], 77 | [ 78 | 'Address' => '1196 Court St', 79 | 'City' => 'York', 80 | 'Postcode' => 'PA 17404 ‎', 81 | 'Country' => 'US' 82 | ], 83 | [ 84 | 'Address' => '25 Barnes St', 85 | 'City' => 'Bel Air', 86 | 'Postcode' => 'MD 21014', 87 | 'Country' => 'US' 88 | ], 89 | ]; 90 | protected static $domains = ['perdu.com', 'silverstripe.org', 'google.be']; 91 | protected static $words = [ 92 | 'lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 93 | 'elit', 'curabitur', 'vel', 'hendrerit', 'libero', 'eleifend', 94 | 'blandit', 'nunc', 'ornare', 'odio', 'ut', 'orci', 95 | 'gravida', 'imperdiet', 'nullam', 'purus', 'lacinia', 'a', 96 | 'pretium', 'quis', 'congue', 'praesent', 'sagittis', 'laoreet', 97 | 'auctor', 'mauris', 'non', 'velit', 'eros', 'dictum', 98 | 'proin', 'accumsan', 'sapien', 'nec', 'massa', 'volutpat', 99 | 'venenatis', 'sed', 'eu', 'molestie', 'lacus', 'quisque', 100 | 'porttitor', 'ligula', 'dui', 'mollis', 'tempus', 'at', 101 | 'magna', 'vestibulum', 'turpis', 'ac', 'diam', 102 | 'tincidunt', 'id', 'condimentum', 'enim', 'sodales', 'in', 103 | 'hac', 'habitasse', 'platea', 'dictumst', 'aenean', 'neque', 104 | 'fusce', 'augue', 'leo', 'eget', 'semper', 'mattis', 105 | 'tortor', 'scelerisque', 'nulla', 'interdum', 'tellus', 106 | 'malesuada', 'rhoncus', 'porta', 'sem', 'aliquet', 107 | 'et', 'nam', 'suspendisse', 'potenti', 'vivamus', 'luctus', 108 | 'fringilla', 'erat', 'donec', 'justo', 'vehicula', 109 | 'ultricies', 'varius', 'ante', 'primis', 'faucibus', 'ultrices', 110 | 'posuere', 'cubilia', 'curae', 'etiam', 'cursus', 111 | 'aliquam', 'quam', 'dapibus', 'nisl', 'feugiat', 'egestas', 112 | 'class', 'aptent', 'taciti', 'sociosqu', 'ad', 'litora', 113 | 'torquent', 'per', 'conubia', 'nostra', 'inceptos', 'himenaeos', 114 | 'phasellus', 'nibh', 'pulvinar', 'vitae', 'urna', 'iaculis', 115 | 'lobortis', 'nisi', 'viverra', 'arcu', 'morbi', 'pellentesque', 116 | 'metus', 'commodo', 'ut', 'facilisis', 'felis', 117 | 'tristique', 'ullamcorper', 'placerat', 'aenean', 'convallis', 118 | 'sollicitudin', 'integer', 'rutrum', 'duis', 'est', 119 | 'etiam', 'bibendum', 'donec', 'pharetra', 'vulputate', 'maecenas', 120 | 'mi', 'fermentum', 'consequat', 'suscipit', 'aliquam', 121 | 'habitant', 'senectus', 'netus', 'fames', 'quisque', 122 | 'euismod', 'curabitur', 'lectus', 'elementum', 'tempor', 123 | 'risus', 'cras' 124 | ]; 125 | 126 | /** 127 | * A random firstname 128 | * 129 | * @return string 130 | */ 131 | public static function firstname() 132 | { 133 | return self::$firstNames[array_rand(self::$firstNames)]; 134 | } 135 | 136 | /** 137 | * A random lastname 138 | * 139 | * @return string 140 | */ 141 | public static function lastname() 142 | { 143 | return self::$lastNames[array_rand(self::$lastNames)]; 144 | } 145 | 146 | /** 147 | * A random name 148 | * 149 | * @return string 150 | */ 151 | public static function name() 152 | { 153 | return self::firstname() . ' ' . self::lastname(); 154 | } 155 | 156 | /** 157 | * A random boolean 158 | * 159 | * @return bool 160 | */ 161 | public static function boolean() 162 | { 163 | return rand(0, 1) == 1; 164 | } 165 | 166 | /** 167 | * A random address 168 | * 169 | * @return array 170 | */ 171 | public static function address() 172 | { 173 | return self::$addresses[array_rand(self::$addresses)]; 174 | } 175 | 176 | /** 177 | * A random address part 178 | * 179 | * @param string $part Address, Street, StreetNumber, City, Postcode or Country 180 | * @return string 181 | */ 182 | public static function addressPart($part) 183 | { 184 | $address = self::address(); 185 | $rpart = $part; 186 | if ($part == 'Street' || $part == 'StreetNumber') { 187 | $rpart = 'Address'; 188 | } 189 | $v = $address[$rpart]; 190 | if ($part == 'Street' || $part == 'StreetNumber') { 191 | $vex = explode(' ', $v); 192 | if ($part == 'Street') { 193 | array_shift($vex); 194 | $v = implode(' ', $vex); 195 | } 196 | if ($part == 'StreetNumber') { 197 | $v = $vex[0]; 198 | } 199 | } 200 | return $v; 201 | } 202 | 203 | /** 204 | * Random float point value 205 | * 206 | * @param int $intMin 207 | * @param int $intMax 208 | * @param int $intDecimals 209 | * @return float 210 | */ 211 | public static function fprand($intMin, $intMax, $intDecimals) 212 | { 213 | if ($intDecimals) { 214 | $intPowerTen = pow(10, $intDecimals); 215 | return rand($intMin, $intMax * $intPowerTen) / $intPowerTen; 216 | } else { 217 | return rand($intMin, $intMax); 218 | } 219 | } 220 | 221 | /** 222 | * A randomized position 223 | * 224 | * @param float $latitude 225 | * @param float $longitude 226 | * @param int $radius 227 | * @return array 228 | */ 229 | public static function latLon($latitude = null, $longitude = null, $radius = 20) 230 | { 231 | if ($latitude === null) { 232 | $latitude = self::$latitude; 233 | } 234 | if ($longitude === null) { 235 | $longitude = self::$longitude; 236 | } 237 | $lng_min = $longitude - $radius / abs(cos(deg2rad($latitude)) * 69); 238 | $lng_max = $longitude + $radius / abs(cos(deg2rad($latitude)) * 69); 239 | $lat_min = $latitude - ($radius / 69); 240 | $lat_max = $latitude + ($radius / 69); 241 | 242 | $rand = self::fprand(0, ($lng_max - $lng_min), 3); 243 | $lng = $lng_min + $rand; 244 | $rand = self::fprand(0, ($lat_max - $lat_min), 3); 245 | $lat = $lat_min + $rand; 246 | 247 | return compact('lat', 'lng', 'lng_min', 'lat_min', 'lng_max', 'lat_max'); 248 | } 249 | 250 | /** 251 | * A random domain 252 | * @return string 253 | */ 254 | public static function domain() 255 | { 256 | return self::$domains[array_rand(self::$domains)]; 257 | } 258 | 259 | /** 260 | * A random website 261 | * 262 | * @return string 263 | */ 264 | public static function website() 265 | { 266 | return 'http://www' . self::domain(); 267 | } 268 | 269 | public static function adorableAvatar($size = 200, $id = null) 270 | { 271 | if (!$id) { 272 | $id = uniqid(); 273 | } 274 | $result = file_get_contents('https://api.adorable.io/avatars/' . $size . '/' . $id); 275 | 276 | return self::storeFakeImage($result, $id . '.png', 'Adorable'); 277 | } 278 | 279 | /** 280 | * Generate some random users 281 | * 282 | * @link https://randomuser.me/documentation 283 | * @param array $params 284 | * @param bool $useDefaultParams 285 | * @return array 286 | */ 287 | public static function randomUser($params = [], $useDefaultParams = true) 288 | { 289 | $defaultParams = [ 290 | 'results' => '20', 291 | 'password' => 'upper,lower,8-12', 292 | 'nat' => 'fr,us,gb,de', 293 | ]; 294 | 295 | if ($useDefaultParams) { 296 | $params = array_merge($defaultParams, $params); 297 | } 298 | $result = file_get_contents('https://randomuser.me/api/?' . http_build_query($params)); 299 | 300 | $data = json_decode($result, JSON_OBJECT_AS_ARRAY); 301 | 302 | if (!empty($data['error'])) { 303 | throw new Exception($data['error']); 304 | } 305 | 306 | return $data['results']; 307 | } 308 | 309 | /** 310 | * Store a fake image 311 | * 312 | * @param string $data 313 | * @param string $name The filename, including extension 314 | * @param string $folder The sub folder where the fake image is stored (default folder is Faker/Uploads) 315 | * @return Image 316 | */ 317 | public static function storeFakeImage($data, $name, $folder = 'Uploads') 318 | { 319 | $filter = new FileNameFilter; 320 | $name = $filter->filter($name); 321 | 322 | $folderName = self::$folder . '/' . $folder; 323 | $filename = $folderName . '/' . $name; 324 | 325 | $image = new Image; 326 | $image->setFromString($data, $filename); 327 | $image->write(); 328 | 329 | return $image; 330 | } 331 | 332 | /** 333 | * Get a random image 334 | * 335 | * Images are generated only once, if folder does not exists in assets 336 | * 337 | * @return Image 338 | */ 339 | public static function image() 340 | { 341 | $path = self::$folder . '/Images'; 342 | $images = Image::get()->where("FileFilename LIKE '$path%'"); 343 | if ($images->count() <= 0) { 344 | $folder = Folder::find_or_make($path); 345 | foreach (range(1, 30) as $i) { 346 | $data = file_get_contents("https://picsum.photos/id/$i/1920/1080"); 347 | $filename = "$path/fake-$i.jpg"; 348 | $imageObj = new Image; 349 | $imageObj->setFromString($data, $filename); 350 | $imageObj->write(); 351 | 352 | // publish this stuff! 353 | if ($imageObj->isInDB() && !$imageObj->isPublished()) { 354 | $imageObj->publishSingle(); 355 | } 356 | } 357 | $images = Image::get()->where("FileFilename LIKE 'Faker/Images%'"); 358 | } 359 | $rand = rand(0, count($images)); 360 | foreach ($images as $key => $image) { 361 | if ($key == $rand) { 362 | // publish this stuff! 363 | if ($image->isInDB() && !$image->isPublished()) { 364 | $image->publishSingle(); 365 | } 366 | 367 | return $image; 368 | } 369 | } 370 | return $images->First(); 371 | } 372 | 373 | /** 374 | * Get a random image unique for a record that can be deleted without side effects 375 | * for other records 376 | * 377 | * @param DataObject $record 378 | * @return Image 379 | */ 380 | public static function ownImage($record) 381 | { 382 | $path = self::$folder; 383 | if ($record->hasMethod('getFolderName')) { 384 | $path = $record->getFolderName(); 385 | $path .= "/" . self::$folder; 386 | } 387 | 388 | $i = rand(1, 1080); 389 | $data = file_get_contents("https://picsum.photos/id/$i/1920/1080"); 390 | 391 | $name = "fake-$i.jpg"; 392 | $filename = "$path/$name"; 393 | 394 | $image = new Image; 395 | $image->setFromString($data, $filename); 396 | $image->write(); 397 | 398 | return $image; 399 | } 400 | 401 | /** 402 | * Get a random record 403 | * 404 | * @param string $class 405 | * @param array $filters 406 | * @return DataObject 407 | */ 408 | public static function record($class, $filters = []) 409 | { 410 | $q = $class::get()->sort('RAND()'); 411 | if (!empty($filters)) { 412 | $q = $q->filter($filters); 413 | } 414 | return $q->first(); 415 | } 416 | 417 | /** 418 | * Get random words 419 | * 420 | * @param int $num 421 | * @param int $num2 422 | * @return string 423 | */ 424 | public static function words($num, $num2 = null) 425 | { 426 | $res = []; 427 | $i = 0; 428 | $total = $num; 429 | if ($num2 !== null) { 430 | $i = rand(0, $num); 431 | $total = $num2; 432 | } 433 | $req = $total - $i; 434 | foreach (array_rand(self::$words, $req) as $key) { 435 | $res[] = self::$words[$key]; 436 | } 437 | return implode(' ', $res); 438 | } 439 | 440 | /** 441 | * Get random sentences 442 | * 443 | * @param int $num 444 | * @param int $num2 445 | * @return string 446 | */ 447 | public static function sentences($num, $num2 = null) 448 | { 449 | $res = []; 450 | $i = 0; 451 | $total = $num; 452 | if ($num2 !== null) { 453 | $i = rand(0, $num); 454 | $total = $num2; 455 | } 456 | $req = $total - $i; 457 | while ($req--) { 458 | $res[] = self::words(5, 10); 459 | } 460 | return implode(".\n", $res); 461 | } 462 | 463 | /** 464 | * Get random paragraphs 465 | * 466 | * @param int $num 467 | * @param int $num2 468 | * @return string 469 | */ 470 | public static function paragraphs($num, $num2 = null) 471 | { 472 | $res = []; 473 | $i = 0; 474 | $total = $num; 475 | if ($num2 !== null) { 476 | $i = rand(0, $num); 477 | $total = $num2; 478 | } 479 | $req = $total - $i; 480 | while ($req--) { 481 | $res[] = "

    " . self::sentences(3, 7) . "

    "; 482 | } 483 | return implode("\n", $res); 484 | } 485 | 486 | /** 487 | * Get a date between two dates 488 | * 489 | * @param string $num 490 | * @param string $num2 491 | * @param string $format 492 | * @return string 493 | */ 494 | public static function date($num, $num2, $format = 'Y-m-d H:i:s') 495 | { 496 | if (is_string($num)) { 497 | $num = strtotime($num); 498 | } 499 | if (is_string($num2)) { 500 | $num2 = strtotime($num2); 501 | } 502 | $rand = rand($num, $num2); 503 | return date($format, $rand); 504 | } 505 | 506 | /** 507 | * Male or female 508 | * 509 | * @return string 510 | */ 511 | public static function male_or_female() 512 | { 513 | return self::pick(['male', 'female']); 514 | } 515 | 516 | /** 517 | * Randomly pick in an array 518 | * 519 | * @param array $arr 520 | * @return array 521 | */ 522 | public static function pick(array $arr) 523 | { 524 | return $arr[array_rand($arr)]; 525 | } 526 | 527 | /** 528 | * Get a random country 529 | * 530 | * @return string 531 | */ 532 | public static function country() 533 | { 534 | $countries = array_values(CountriesList::get()); 535 | return $countries[array_rand($countries)]; 536 | } 537 | 538 | /** 539 | * Get a random country code 540 | * 541 | * @return string 542 | */ 543 | public static function countryCode() 544 | { 545 | $countries = array_keys(CountriesList::get()); 546 | return $countries[array_rand($countries)]; 547 | } 548 | } 549 | -------------------------------------------------------------------------------- /src/Helpers/DevUtils.php: -------------------------------------------------------------------------------- 1 | getProperty($prop); 20 | $refProperty->setAccessible(true); 21 | $refProperty->setValue($obj, $val); 22 | } 23 | 24 | /** 25 | * @param object $obj 26 | * @param string $prop 27 | * @param callable $cb 28 | * @return void 29 | */ 30 | public static function updatePropCb(object $obj, string $prop, callable $cb): void 31 | { 32 | $refObject = new ReflectionObject($obj); 33 | $refProperty = $refObject->getProperty($prop); 34 | $refProperty->setAccessible(true); 35 | $refProperty->setValue($obj, $cb($refProperty->getValue($obj))); 36 | } 37 | 38 | /** 39 | * @param object $obj 40 | * @param string $prop 41 | * @return mixed 42 | */ 43 | public static function getProp(object $obj, string $prop) 44 | { 45 | $refObject = new ReflectionObject($obj); 46 | $refProperty = $refObject->getProperty($prop); 47 | $refProperty->setAccessible(true); 48 | return $refProperty->getValue($obj); 49 | } 50 | 51 | /** 52 | * @param string $class 53 | * @param string $prop 54 | * @return mixed 55 | */ 56 | public static function getStaticProp(string $class, string $prop) 57 | { 58 | $refClass = new ReflectionClass($class); 59 | return $refClass->getStaticPropertyValue($prop); 60 | } 61 | 62 | /** 63 | * @param string $class 64 | * @param string $prop 65 | * @param mixed $val 66 | * @return void 67 | */ 68 | public static function updateStaticProp(string $class, string $prop, $val) 69 | { 70 | $refClass = new ReflectionClass($class); 71 | $refClass->setStaticPropertyValue($prop, $val); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Helpers/DuplicateMembersMerger.php: -------------------------------------------------------------------------------- 1 | byID($r->ID); 40 | } 41 | if (!$latest) { 42 | $latest = $r; 43 | } else { 44 | if (strtotime($r->LastEdited) > strtotime($latest->LastEdited)) { 45 | $latest = $r; 46 | } 47 | } 48 | if (!$oldest) { 49 | $oldest = $r; 50 | } else { 51 | if ($r->ID < $oldest->ID) { 52 | $oldest = $r->ID; 53 | } 54 | } 55 | 56 | $all[] = $r; 57 | } 58 | 59 | foreach ($all as $a) { 60 | if ($a->ID == $oldest->ID) { 61 | continue; 62 | } 63 | $all_but_oldest[] = $a; 64 | } 65 | 66 | foreach ($all as $a) { 67 | if ($a->ID == $latest->ID) { 68 | continue; 69 | } 70 | $all_but_latest[] = $a; 71 | } 72 | 73 | if (class_exists('Subsite')) { 74 | Subsite::$disable_subsite_filter = true; 75 | } 76 | 77 | Config::modify()->set(DataObject::class, 'validation_enabled', false); 78 | 79 | // Rewrite all relations so everything is pointing to oldest 80 | // For some reason, the code in merge fails to do this properly 81 | $tables = DataObject::getSchema()->getTableNames(); 82 | $objects = ClassInfo::subclassesFor('DataObject'); 83 | foreach ($objects as $o) { 84 | $config = $o::config(); 85 | if ($config->has_one) { 86 | foreach ($config->has_one as $name => $class) { 87 | if ($class == 'Member') { 88 | $table = ClassInfo::table_for_object_field( 89 | $o, 90 | $name . 'ID' 91 | ); 92 | if ($table && in_array(strtolower($table), $tables)) { 93 | foreach ($all_but_oldest as $a) { 94 | $sql = "UPDATE $table SET " . $name . 'ID = ' . $oldest->ID . ' WHERE ' . $name . 'ID = ' . $a->ID; 95 | DB::alteration_message($sql); 96 | DB::query($sql); 97 | } 98 | } 99 | } 100 | } 101 | } 102 | if ($config->has_many) { 103 | foreach ($config->has_many as $name => $class) { 104 | if ($class == 'Member') { 105 | $table = ClassInfo::table_for_object_field( 106 | $o, 107 | $name . 'ID' 108 | ); 109 | if ($table && in_array(strtolower($table), $tables)) { 110 | foreach ($all_but_oldest as $a) { 111 | $sql = "UPDATE $table SET " . $name . 'ID = ' . $oldest->ID . ' WHERE ' . $name . 'ID = ' . $a->ID; 112 | DB::alteration_message($sql); 113 | DB::query($sql); 114 | } 115 | } 116 | } 117 | } 118 | } 119 | if ($config->many_many) { 120 | foreach ($config->many_many as $name => $class) { 121 | if ($class == 'Member') { 122 | $table = ClassInfo::table_for_object_field( 123 | $o, 124 | $name . 'ID' 125 | ); 126 | if ($table && in_array(strtolower($table), $tables)) { 127 | foreach ($all_but_oldest as $a) { 128 | $sql = "UPDATE $table SET " . $name . 'ID = ' . $oldest->ID . ' WHERE ' . $name . 'ID = ' . $a->ID; 129 | DB::alteration_message($sql); 130 | DB::query($sql); 131 | } 132 | } 133 | } 134 | } 135 | } 136 | } 137 | 138 | // Now, we update to oldest record with the latest info 139 | 140 | $orgOldest = $oldest; 141 | $oldest->merge($latest, 'right', false); 142 | foreach ($all_but_oldest as $a) { 143 | $a->delete(); 144 | } 145 | 146 | try { 147 | $oldest->write(); 148 | } catch (Exception $ex) { 149 | $orgOldest->write(); 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/Helpers/EmptySpaceFinder.php: -------------------------------------------------------------------------------- 1 | [\s]+$/'; 12 | 13 | public static function findSpacesInFiles($files) 14 | { 15 | echo '
    ';
    16 |         echo "Finding opened or closed tags ...\n\n";
    17 | 
    18 |         $openings = [];
    19 |         $closings = [];
    20 |         foreach ($files as $file) {
    21 |             $content = file_get_contents($file);
    22 | 
    23 |             $matches = null;
    24 |             preg_match_all(self::REGEX_OPENING, $content, $matches);
    25 | 
    26 |             if (!empty($matches[0])) {
    27 |                 $openings[] = $file;
    28 |             }
    29 | 
    30 |             $matches = null;
    31 |             preg_match_all(self::REGEX_CLOSING, $content, $matches);
    32 | 
    33 |             if (!empty($matches[0])) {
    34 |                 $closings[] = $file;
    35 |             }
    36 |         }
    37 | 
    38 |         if (!empty($openings)) {
    39 |             echo "Files with opening tags that may need fixing\n";
    40 |             foreach ($openings as $file) {
    41 |                 echo "$file\n";
    42 |             }
    43 |             echo "***\n";
    44 |         }
    45 |         if (!empty($closings)) {
    46 |             echo "Files with closing tags that may need fixing\n";
    47 |             foreach ($closings as $file) {
    48 |                 echo "$file\n";
    49 |             }
    50 |             echo "***\n";
    51 |         }
    52 | 
    53 |         echo "\nDone!";
    54 |         echo '
    '; 55 | die(); 56 | } 57 | 58 | public static function findSpacesInIncludedFiles() 59 | { 60 | $files = get_included_files(); 61 | self::findSpacesInFiles($files); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Helpers/EnvironmentChecker.php: -------------------------------------------------------------------------------- 1 | getRequest(); 19 | } 20 | return in_array($request->getIP(), ['127.0.0.1', '::1', '1']); 21 | } 22 | 23 | /** 24 | * Temp folder should always be there 25 | * 26 | * @return void 27 | */ 28 | public static function ensureTempFolderExists() 29 | { 30 | $tempFolder = Director::baseFolder() . '/silverstripe-cache'; 31 | if (!is_dir($tempFolder)) { 32 | mkdir($tempFolder, 0755); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Helpers/FileHelper.php: -------------------------------------------------------------------------------- 1 | 0 && $lines >= 0) { 44 | // Figure out how far back we should jump 45 | $seek = min(ftell($f), $buffer); 46 | // Do the jump (backwards, relative to where we are) 47 | fseek($f, -$seek, SEEK_CUR); 48 | // Read a chunk and prepend it to our output 49 | $output = ($chunk = fread($f, $seek)) . $output; 50 | // Jump back to where we started reading 51 | fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR); 52 | // Decrease our line counter 53 | $lines -= substr_count($chunk, "\n"); 54 | } 55 | // While we have too many lines 56 | // (Because of buffer size we might have read too many) 57 | while ($lines++ < 0) { 58 | // Find first newline and remove all text before that 59 | $output = substr($output, strpos($output, "\n") + 1); 60 | } 61 | // Close file and return 62 | fclose($f); 63 | return trim($output); 64 | } 65 | 66 | /** 67 | * Recursively remove a dir 68 | * 69 | * @param string $dir 70 | * @return bool 71 | */ 72 | public static function rmDir($dir) 73 | { 74 | if (!is_dir($dir)) { 75 | return false; 76 | } 77 | $it = new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS); 78 | $files = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::CHILD_FIRST); 79 | foreach ($files as $file) { 80 | if ($file->isDir()) { 81 | rmdir($file->getRealPath()); 82 | } else { 83 | unlink($file->getRealPath()); 84 | } 85 | } 86 | return rmdir($dir); 87 | } 88 | 89 | /** 90 | * Check if a directory contains children 91 | * 92 | * @link https://stackoverflow.com/questions/6786014/php-fastest-way-to-find-if-directory-has-children 93 | * @param string $dir 94 | * @return bool 95 | */ 96 | public static function dirContainsChildren($dir) 97 | { 98 | $result = false; 99 | if ($dh = opendir($dir)) { 100 | while (!$result && ($file = readdir($dh)) !== false) { 101 | $result = $file !== "." && $file !== ".."; 102 | } 103 | closedir($dh); 104 | } 105 | return $result; 106 | } 107 | 108 | /** 109 | * @link https://www.digitalocean.com/community/questions/proper-permissions-for-web-server-s-directory 110 | * @param string $dir 111 | * @return bool 112 | */ 113 | public static function ensureDir($dir) 114 | { 115 | if (!is_dir($dir)) { 116 | return mkdir($dir, 0755, true); 117 | } 118 | return true; 119 | } 120 | 121 | /** 122 | * @param int $bytes 123 | * @param integer $decimals 124 | * @return string 125 | */ 126 | public static function humanFilesize($bytes, $decimals = 2) 127 | { 128 | if ($bytes < 1024) { 129 | return $bytes . ' B'; 130 | } 131 | $factor = floor(log($bytes, 1024)); 132 | return sprintf("%.{$decimals}f ", $bytes / pow(1024, $factor)) . ['B', 'KB', 'MB', 'GB', 'TB', 'PB'][$factor]; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Helpers/SubsiteHelper.php: -------------------------------------------------------------------------------- 1 | getSubsiteId(); 37 | } 38 | return 0; 39 | } 40 | 41 | /** 42 | * @return Subsite 43 | */ 44 | public static function currentSubsite() 45 | { 46 | $id = self::currentSubsiteID(); 47 | if (self::usesSubsite()) { 48 | return DataObject::get_by_id(Subsite::class, $id); 49 | } 50 | return false; 51 | } 52 | 53 | /** 54 | * Do we have the subsite module installed 55 | * TODO: check if it might be better to use module manifest instead? 56 | * 57 | * @return bool 58 | */ 59 | public static function usesSubsite() 60 | { 61 | return class_exists(SubsiteState::class); 62 | } 63 | 64 | /** 65 | * @return bool 66 | */ 67 | public static function subsiteFilterDisabled() 68 | { 69 | if (!self::usesSubsite()) { 70 | return true; 71 | } 72 | return Subsite::$disable_subsite_filter; 73 | } 74 | 75 | /** 76 | * Enable subsite filter and store previous state 77 | * 78 | * @return void 79 | */ 80 | public static function enableFilter() 81 | { 82 | if (!self::usesSubsite()) { 83 | return; 84 | } 85 | self::$previousState = Subsite::$disable_subsite_filter; 86 | Subsite::$disable_subsite_filter = false; 87 | } 88 | 89 | /** 90 | * Disable subsite filter and store previous state 91 | * 92 | * @return void 93 | */ 94 | public static function disableFilter() 95 | { 96 | if (!self::usesSubsite()) { 97 | return; 98 | } 99 | self::$previousState = Subsite::$disable_subsite_filter; 100 | Subsite::$disable_subsite_filter = true; 101 | } 102 | 103 | /** 104 | * Restore subsite filter based on previous set (set when called enableFilter or disableFilter) 105 | */ 106 | public static function restoreFilter() 107 | { 108 | if (!self::usesSubsite()) { 109 | return; 110 | } 111 | Subsite::$disable_subsite_filter = self::$previousState; 112 | } 113 | 114 | /** 115 | * @return int 116 | */ 117 | public static function SubsiteIDFromSession() 118 | { 119 | $session = Controller::curr()->getRequest()->getSession(); 120 | if ($session) { 121 | return $session->get('SubsiteID'); 122 | } 123 | return 0; 124 | } 125 | 126 | /** 127 | * Typically call this on PageController::init 128 | * This is due to InitStateMiddleware not using session in front end and not persisting get var parameters 129 | * 130 | * @param HTTPRequest $request 131 | * @return int 132 | */ 133 | public static function forceSubsiteFromRequest(HTTPRequest $request) 134 | { 135 | $subsiteID = $request->getVar('SubsiteID'); 136 | if ($subsiteID) { 137 | $request->getSession()->set('ForcedSubsiteID', $subsiteID); 138 | } else { 139 | $subsiteID = $request->getSession()->get('ForcedSubsiteID'); 140 | } 141 | if ($subsiteID) { 142 | self::changeSubsite($subsiteID, true); 143 | } 144 | return $subsiteID; 145 | } 146 | 147 | /** 148 | * @param string $ID 149 | * @param bool $flush 150 | * @return void 151 | */ 152 | public static function changeSubsite($ID, $flush = null) 153 | { 154 | if (!self::usesSubsite()) { 155 | return; 156 | } 157 | self::$previousSubsite = self::currentSubsiteID(); 158 | 159 | // Do this otherwise changeSubsite has no effect if false 160 | SubsiteState::singleton()->setUseSessions(true); 161 | Subsite::changeSubsite($ID); 162 | // This can help avoiding getting static objects like SiteConfig 163 | if ($flush !== null && $flush) { 164 | DataObject::reset(); 165 | } 166 | } 167 | 168 | /** 169 | * @param bool $flush 170 | * @return void 171 | */ 172 | public static function restoreSubsite($flush = null) 173 | { 174 | if (!self::usesSubsite()) { 175 | return; 176 | } 177 | Subsite::changeSubsite(self::$previousSubsite, $flush); 178 | } 179 | 180 | /** 181 | * @return array 182 | */ 183 | public static function listSubsites() 184 | { 185 | if (!self::usesSubsite()) { 186 | return []; 187 | } 188 | return Subsite::get()->map(); 189 | } 190 | 191 | /** 192 | * Execute the callback in given subsite 193 | * 194 | * @param int $ID Subsite ID or 0 for main site 195 | * @param callable $cb 196 | * @return void 197 | */ 198 | public static function withSubsite($ID, $cb) 199 | { 200 | $currentID = self::currentSubsiteID(); 201 | SubsiteState::singleton()->setSubsiteId($ID); 202 | $cb(); 203 | SubsiteState::singleton()->setSubsiteId($currentID); 204 | } 205 | 206 | /** 207 | * Execute the callback in all subsites 208 | * 209 | * @param callable $cb 210 | * @param bool $încludeMainSite 211 | * @return void 212 | */ 213 | public static function withSubsites($cb, $includeMainSite = true) 214 | { 215 | if (!self::usesSubsite()) { 216 | $cb(); 217 | return; 218 | } 219 | 220 | if ($includeMainSite) { 221 | SubsiteState::singleton()->setSubsiteId(0); 222 | $cb(0); 223 | } 224 | 225 | $currentID = self::currentSubsiteID(); 226 | $subsites = Subsite::get(); 227 | foreach ($subsites as $subsite) { 228 | // TODO: maybe use changeSubsite instead? 229 | SubsiteState::singleton()->setSubsiteId($subsite->ID); 230 | $cb($subsite->ID); 231 | } 232 | SubsiteState::singleton()->setSubsiteId($currentID); 233 | } 234 | 235 | public static function SiteConfig($SubsiteID = 0) 236 | { 237 | if (!$SubsiteID) { 238 | $SubsiteID = self::currentSubsiteID(); 239 | } 240 | $SiteConfig = SiteConfig::get()->setDataQueryParam('Subsite.Filter', false)->filter( 241 | [ 242 | 'SubsiteID' => $SubsiteID, 243 | ] 244 | )->first(); 245 | if (!$SiteConfig) { 246 | $SiteConfig = SiteConfig::current_site_config(); 247 | } 248 | if (!$SiteConfig) { 249 | $SiteConfig = new SiteConfig(); 250 | } 251 | return $SiteConfig; 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/Tasks/ClearCacheFolderTask.php: -------------------------------------------------------------------------------- 1 | request = $request; 21 | 22 | $folder = Director::baseFolder() . '/silverstripe-cache'; 23 | $create = $_GET['create'] ?? false; 24 | if (!is_dir($folder)) { 25 | if ($create) { 26 | mkdir($folder, 0755); 27 | } else { 28 | throw new Exception("silverstripe-cache folder does not exist in root"); 29 | } 30 | } 31 | 32 | $result = FileHelper::rmDir($folder); 33 | if ($result) { 34 | $this->message("Removed $folder"); 35 | } else { 36 | $this->message("Failed to remove $folder", "error"); 37 | } 38 | $result = mkdir($folder, 0755); 39 | if ($result) { 40 | $this->message("A new folder has been created at $folder"); 41 | } else { 42 | $this->message("Failed to create a new folder at $folder", "error"); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Tasks/DisabledMigrationTask.php: -------------------------------------------------------------------------------- 1 | request = $request; 33 | 34 | $this->addOption("go", "Tick this to proceed", false); 35 | $this->addOption("remove_files", "Remove db files", false); 36 | $this->addOption("remove_local", "Remove local files", false); 37 | 38 | $options = $this->askOptions(); 39 | 40 | $go = $options['go']; 41 | $remove_files = $options['remove_files']; 42 | $remove_local = $options['remove_local']; 43 | 44 | if (!$go) { 45 | echo ('Previewing what this task is about to do.'); 46 | } else { 47 | echo ("Let's clean this up!"); 48 | } 49 | echo ('
    '); 50 | if ($remove_files) { 51 | $this->removeFiles($request, $go); 52 | } 53 | if ($remove_local) { 54 | $this->removeLocalFiles($request, $go); 55 | } 56 | } 57 | 58 | protected function removeLocalFiles($request, $go = false) 59 | { 60 | $iter = new RecursiveDirectoryIterator(ASSETS_PATH); 61 | $iter2 = new RecursiveIteratorIterator($iter); 62 | 63 | foreach ($iter2 as $file) { 64 | // Ignore roots and _ 65 | $startsWithSlash = strpos($file->getName(), '_') === 0; 66 | $hasVariant = strpos($file->getName(), '__') !== false; 67 | if ($startsWithSlash || $hasVariant) { 68 | // $this->message("Ignore " . $file->getPath()); 69 | continue; 70 | } 71 | 72 | // Check for empty dirs 73 | if ($file->isDir()) { 74 | // ignores .dot files 75 | $dirFiles = scandir($file->getPath()); 76 | $empty = (count($dirFiles) - 2) === 0; 77 | if ($empty) { 78 | $this->message($file->getPath() . " is empty"); 79 | if ($go) { 80 | rmdir($file->getPath()); 81 | } 82 | } 83 | continue; 84 | } 85 | 86 | // Check for files not matching anything in the db 87 | $thisPath = str_replace(ASSETS_PATH, "", $file->getPath()); 88 | // $this->message($thisPath); 89 | // $dbFile = File::get()->filter("FileFilename", $thisPath); 90 | } 91 | } 92 | 93 | protected function removeFiles($request, $go = false) 94 | { 95 | $conn = DB::get_conn(); 96 | $schema = DB::get_schema(); 97 | $dataObjectSchema = DataObject::getSchema(); 98 | $tableList = $schema->tableList(); 99 | 100 | $files = File::get(); 101 | 102 | if ($go) { 103 | $conn->transactionStart(); 104 | } 105 | 106 | $i = 0; 107 | 108 | /** @var File $file */ 109 | foreach ($files as $file) { 110 | if ($file instanceof Folder) { 111 | continue; 112 | } 113 | $path = self::getFullPath($file); 114 | $hashPath = self::getHashPath($file); 115 | if (!trim($file->getRelativePath(), '/')) { 116 | $this->message("#{$file->ID}: path is empty"); 117 | if ($go) { 118 | // $file->delete(); 119 | self::deleteFile($file->ID); 120 | $i++; 121 | } 122 | } 123 | if (!file_exists($path) && !file_exists($hashPath)) { 124 | $this->message("#{$file->ID}: $path does not exist"); 125 | if ($go) { 126 | // $file->delete(); 127 | self::deleteFile($file->ID); 128 | $i++; 129 | } 130 | } else { 131 | $this->message("#{$file->ID}: $path is valid", "success"); 132 | } 133 | if ($go && $i % 100 == 0) { 134 | $conn->transactionEnd(); 135 | $conn->transactionStart(); 136 | } 137 | } 138 | 139 | if ($go) { 140 | $conn->transactionEnd(); 141 | } 142 | } 143 | 144 | /** 145 | * ORM is just too slow for this 146 | * 147 | * @param int $ID 148 | * @return void 149 | */ 150 | public static function deleteFile($ID) 151 | { 152 | DB::prepared_query("DELETE FROM File WHERE ID = ?", [$ID]); 153 | DB::prepared_query("DELETE FROM File_Live WHERE ID = ?", [$ID]); 154 | DB::prepared_query("DELETE FROM File_Versions WHERE RecordID = ?", [$ID]); 155 | DB::prepared_query("DELETE FROM File_ViewerGroups WHERE FileID = ?", [$ID]); 156 | DB::prepared_query("DELETE FROM File_EditorGroups WHERE FileID = ?", [$ID]); 157 | } 158 | 159 | public static function getFullPath(File $file) 160 | { 161 | return ASSETS_PATH . '/' . $file->getRelativePath(); 162 | } 163 | 164 | public static function getHashPath(File $file) 165 | { 166 | $path = $file->getRelativePath(); 167 | $parts = explode('/', $path); 168 | $name = array_pop($parts); 169 | $folder = implode("/", $parts); 170 | 171 | $full = ASSETS_PATH . '/' . $folder . '/' . substr($file->getHash(), 0, 10) . '/' . $name; 172 | $full = str_replace('//', '/', $full); 173 | return $full; 174 | } 175 | 176 | public function getProtectedFullPath(File $file) 177 | { 178 | return self::getBaseProtectedPath() . '/' . $file->getRelativePath(); 179 | } 180 | 181 | public static function getBaseProtectedPath() 182 | { 183 | // Use environment defined path or default location is under assets 184 | if ($path = Environment::getEnv('SS_PROTECTED_ASSETS_PATH')) { 185 | return $path; 186 | } 187 | 188 | // Default location 189 | return ASSETS_PATH . '/' . Config::inst()->get(ProtectedAssetAdapter::class, 'secure_folder'); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/Tasks/DropUnusedDatabaseObjectsTask.php: -------------------------------------------------------------------------------- 1 | request = $request; 49 | 50 | $this->addOption("tables", "Clean unused tables", true); 51 | $this->addOption("fields", "Clean unused fields", true); 52 | $this->addOption("reorder", "Reorder fields", false); 53 | $this->addOption("go", "Tick this to proceed", false); 54 | 55 | $options = $this->askOptions(); 56 | 57 | $tables = $options['tables']; 58 | $fields = $options['fields']; 59 | $reorder = $options['reorder']; 60 | $go = $options['go']; 61 | 62 | if (!$go) { 63 | echo ('Previewing what this task is about to do.'); 64 | } else { 65 | echo ("Let's clean this up!"); 66 | } 67 | echo ('
    '); 68 | if ($tables) { 69 | $this->removeTables($request, $go); 70 | } 71 | if ($fields) { 72 | $this->removeFields($request, $go); 73 | } 74 | if ($reorder) { 75 | $this->reorderFields($request, $go); 76 | } 77 | } 78 | 79 | /** 80 | * @param HTTPRequest $request 81 | * @param bool $go 82 | * @return void 83 | */ 84 | protected function reorderFields($request, $go = false) 85 | { 86 | $conn = DB::get_conn(); 87 | $schema = DB::get_schema(); 88 | $dataObjectSchema = DataObject::getSchema(); 89 | $classes = $this->getClassesWithTables(); 90 | $tableList = $schema->tableList(); 91 | 92 | $this->message('

    Fields order

    '); 93 | 94 | foreach ($classes as $class) { 95 | /** @var \SilverStripe\ORM\DataObject $singl */ 96 | $singl = $class::singleton(); 97 | $baseClass = $singl->baseClass(); 98 | $table = $dataObjectSchema->tableName($class); 99 | $lcTable = strtolower($table); 100 | 101 | // It does not exist in the list, no need to worry about 102 | if (!isset($tableList[$lcTable])) { 103 | continue; 104 | } 105 | 106 | $fields = $dataObjectSchema->databaseFields($class); 107 | $baseFields = $dataObjectSchema->databaseFields($baseClass); 108 | 109 | $realFields = $fields; 110 | if ($baseClass != $class) { 111 | foreach ($baseFields as $k => $v) { 112 | if ($k == "ID") { 113 | continue; 114 | } 115 | unset($realFields[$k]); 116 | } 117 | 118 | // When extending multiple classes it's a mess to track, eg SubsitesVirtualPage 119 | if (isset($realFields['VersionID'])) { 120 | unset($realFields['VersionID']); 121 | } 122 | } 123 | 124 | // We must pass the regular table name 125 | $list = $schema->fieldList($table); 126 | 127 | $fields_keys = array_keys($realFields); 128 | $list_keys = array_keys($list); 129 | 130 | if (json_encode($fields_keys) == json_encode($list_keys)) { 131 | continue; 132 | } 133 | 134 | $fieldsThatNeedToMove = []; 135 | foreach ($fields_keys as $k => $v) { 136 | if (!isset($list_keys[$k])) { 137 | continue; // not sure why 138 | } 139 | if ($list_keys[$k] != $v) { 140 | $fieldsThatNeedToMove[] = $v; 141 | } 142 | } 143 | 144 | if ($go) { 145 | $this->message("$table: moving " . implode(", ", $fieldsThatNeedToMove)); 146 | 147 | // $conn->transactionStart(); 148 | // fields contains the right order (the one from the codebase) 149 | $after = "first"; 150 | foreach ($fields_keys as $k => $v) { 151 | if (isset($list_keys[$k]) && $list_keys[$k] != $v) { 152 | $col = $v; 153 | $def = $list[$v] ?? null; 154 | if (!$def) { 155 | // This happens when extending another model 156 | $this->message("Ignore $v that has no definition", "error"); 157 | continue; 158 | } 159 | // you CANNOT combine multiple columns reordering in a single ALTER TABLE statement. 160 | $sql = "ALTER TABLE `$table` MODIFY `$col` $def $after"; 161 | $this->message($sql); 162 | try { 163 | $conn->query($sql); 164 | } catch (Exception $e) { 165 | $this->message($e->getMessage(), "error"); 166 | } 167 | } 168 | $after = "after $v"; 169 | } 170 | // $conn->transactionEnd(); 171 | } else { 172 | $this->message("$table: would move " . implode(", ", $fieldsThatNeedToMove)); 173 | } 174 | } 175 | } 176 | 177 | /** 178 | * @param HTTPRequest $request 179 | * @param bool $go 180 | * @return void 181 | */ 182 | protected function removeFields($request, $go = false) 183 | { 184 | $conn = DB::get_conn(); 185 | $schema = DB::get_schema(); 186 | $dataObjectSchema = DataObject::getSchema(); 187 | $classes = $this->getClassesWithTables(); 188 | $tableList = $schema->tableList(); 189 | 190 | $this->message('

    Fields

    '); 191 | 192 | $empty = true; 193 | 194 | $processedTables = []; 195 | foreach ($classes as $class) { 196 | /** @var \SilverStripe\ORM\DataObject $singl */ 197 | $singl = $class::singleton(); 198 | $baseClass = $singl->baseClass(); 199 | $table = $dataObjectSchema->tableName($baseClass); 200 | $lcTable = strtolower($table); 201 | 202 | if (in_array($table, $processedTables)) { 203 | continue; 204 | } 205 | // It does not exist in the list, no need to worry about 206 | if (!isset($tableList[$lcTable])) { 207 | continue; 208 | } 209 | $processedTables[] = $table; 210 | $toDrop = []; 211 | 212 | $fields = $dataObjectSchema->databaseFields($class); 213 | // We must pass the regular table name 214 | $list = $schema->fieldList($table); 215 | // We can compare DataObject schema with actual schema 216 | foreach ($list as $fieldName => $type) { 217 | /// Never drop ID 218 | if ($fieldName == 'ID') { 219 | continue; 220 | } 221 | if (!isset($fields[$fieldName])) { 222 | $toDrop[] = $fieldName; 223 | } 224 | } 225 | 226 | if (!empty($toDrop)) { 227 | $empty = false; 228 | if ($go) { 229 | $this->dropColumns($table, $toDrop); 230 | $this->message("Dropped " . implode(',', $toDrop) . " for $table", "obsolete"); 231 | } else { 232 | $this->message("Would drop " . implode(',', $toDrop) . " for $table", "obsolete"); 233 | } 234 | } 235 | 236 | // Many many support if has own base table 237 | $many_many = $singl::config()->many_many; 238 | foreach ($many_many as $manyName => $manyClass) { 239 | $toDrop = []; 240 | 241 | // No polymorphism support 242 | if (is_array($manyClass)) { 243 | continue; 244 | } 245 | 246 | // This is very naive and only works in basic cases 247 | $manyTable = $table . '_' . $manyName; 248 | if (!$schema->hasTable($manyTable)) { 249 | continue; 250 | } 251 | $baseManyTable = $dataObjectSchema->tableName($manyClass); 252 | $list = $schema->fieldList($manyTable); 253 | $props = $singl::config()->many_many_extraFields[$manyName] ?? []; 254 | if (empty($props)) { 255 | continue; 256 | } 257 | 258 | // We might miss some! 259 | $validNames = array_merge([ 260 | 'ID', $baseManyTable . 'ID', $table . 'ID', $table . 'ID', 'ChildID', 'SubsiteID', 261 | ], array_keys($props)); 262 | foreach ($list as $fieldName => $fieldDef) { 263 | if (!in_array($fieldName, $validNames)) { 264 | $toDrop[] = $fieldName; 265 | } 266 | } 267 | 268 | if (!empty($toDrop)) { 269 | $empty = false; 270 | if ($go) { 271 | $this->dropColumns($manyTable, $toDrop); 272 | $this->message("Dropped " . implode(',', $toDrop) . " for $manyTable ($table)", "obsolete"); 273 | } else { 274 | $this->message("Would drop " . implode(',', $toDrop) . " for $manyTable ($table)", "obsolete"); 275 | } 276 | } 277 | } 278 | 279 | // Localised fields support 280 | if ($singl->hasExtension("\\TractorCow\\Fluent\\Extension\\FluentExtension")) { 281 | $toDrop = []; 282 | $localeTable = $table . '_Localised'; 283 | //@phpstan-ignore-next-line 284 | $localeFields = $singl->getLocalisedFields($baseClass); 285 | $localeList = $schema->fieldList($localeTable); 286 | foreach ($localeList as $fieldName => $type) { 287 | /// Never drop locale fields 288 | if (in_array($fieldName, ['ID', 'RecordID', 'Locale'])) { 289 | continue; 290 | } 291 | if (!isset($localeFields[$fieldName])) { 292 | $toDrop[] = $fieldName; 293 | } 294 | } 295 | if (!empty($toDrop)) { 296 | $empty = false; 297 | if ($go) { 298 | $this->dropColumns($localeTable, $toDrop); 299 | $this->message("Dropped " . implode(',', $toDrop) . " for $localeTable", "obsolete"); 300 | } else { 301 | $this->message("Would drop " . implode(',', $toDrop) . " for $localeTable", "obsolete"); 302 | } 303 | } 304 | } 305 | } 306 | 307 | if ($empty) { 308 | $this->message("No fields to remove", "repaired"); 309 | } 310 | } 311 | 312 | /** 313 | * @param HTTPRequest $request 314 | * @param bool $go 315 | * @return void 316 | */ 317 | protected function removeTables($request, $go = false) 318 | { 319 | $conn = DB::get_conn(); 320 | $schema = DB::get_schema(); 321 | $dataObjectSchema = DataObject::getSchema(); 322 | $classes = $this->getClassesWithTables(); 323 | $allDataObjects = array_values($this->getValidDataObjects()); 324 | $tableList = $schema->tableList(); 325 | $tablesToRemove = $tableList; 326 | 327 | $this->message('

    Tables

    '); 328 | 329 | foreach ($classes as $class) { 330 | /** @var \SilverStripe\ORM\DataObject $singl */ 331 | $singl = $class::singleton(); 332 | $table = $dataObjectSchema->tableName($class); 333 | $lcTable = strtolower($table); 334 | 335 | // It does not exist in the list, keep to remove later 336 | if (!isset($tableList[$lcTable])) { 337 | continue; 338 | } 339 | 340 | self::removeFromArray($lcTable, $tablesToRemove); 341 | // Remove from the list versioned tables 342 | if ($singl->hasExtension(Versioned::class)) { 343 | self::removeFromArray($lcTable . '_live', $tablesToRemove); 344 | self::removeFromArray($lcTable . '_versions', $tablesToRemove); 345 | } 346 | // Remove from the list fluent tables 347 | if ($singl->hasExtension("\\TractorCow\\Fluent\\Extension\\FluentExtension")) { 348 | self::removeFromArray($lcTable . '_localised', $tablesToRemove); 349 | self::removeFromArray($lcTable . '_localised_live', $tablesToRemove); 350 | self::removeFromArray($lcTable . '_localised_versions', $tablesToRemove); 351 | } 352 | 353 | // Relations 354 | $hasMany = $class::config()->has_many; 355 | if (!empty($hasMany)) { 356 | foreach ($hasMany as $rel => $obj) { 357 | self::removeFromArray($lcTable . '_' . strtolower($rel), $tablesToRemove); 358 | } 359 | } 360 | // We catch relations without own classes later on 361 | $manyMany = $class::config()->many_many; 362 | if (!empty($manyMany)) { 363 | foreach ($manyMany as $rel => $obj) { 364 | self::removeFromArray($lcTable . '_' . strtolower($rel), $tablesToRemove); 365 | } 366 | } 367 | } 368 | 369 | //at this point, we should only have orphans table in dbTables var 370 | foreach ($tablesToRemove as $lcTable => $table) { 371 | // Remove many_many tables without own base table 372 | if (strpos($table, '_') !== false) { 373 | $parts = explode('_', $table); 374 | $potentialClass = $parts[0]; 375 | $potentialRelation = $parts[1]; 376 | foreach ($allDataObjects as $dataObjectClass) { 377 | $classParts = explode('\\', $dataObjectClass); 378 | $tableClass = end($classParts); 379 | if ($tableClass == $potentialClass) { 380 | $manyManyRelations = $dataObjectClass::config()->many_many; 381 | if (isset($manyManyRelations[$potentialRelation])) { 382 | unset($tablesToRemove[$lcTable]); 383 | continue 2; 384 | } 385 | } 386 | } 387 | } 388 | if ($go) { 389 | DB::query('DROP TABLE `' . $table . '`'); 390 | $this->message("Dropped $table", 'obsolete'); 391 | } else { 392 | $this->message("Would drop $table", 'obsolete'); 393 | } 394 | } 395 | 396 | if (empty($tablesToRemove)) { 397 | $this->message("No table to remove", "repaired"); 398 | } 399 | } 400 | 401 | /** 402 | * @return array 403 | */ 404 | protected function getClassesWithTables() 405 | { 406 | return ClassInfo::dataClassesFor(DataObject::class); 407 | } 408 | 409 | /** 410 | * @param mixed $val 411 | * @param array $arr 412 | * @return void 413 | */ 414 | public static function removeFromArray($val, &$arr) 415 | { 416 | if (isset($arr[$val])) { 417 | unset($arr[$val]); 418 | } 419 | } 420 | 421 | /** 422 | * @param string $table 423 | * @param array $columns 424 | * @return void 425 | */ 426 | public function dropColumns($table, $columns) 427 | { 428 | switch (get_class(DB::get_conn())) { 429 | case \SilverStripe\SQLite\SQLite3Database::class: 430 | case 'SQLite3Database': 431 | $this->sqlLiteDropColumns($table, $columns); 432 | break; 433 | default: 434 | $this->sqlDropColumns($table, $columns); 435 | break; 436 | } 437 | } 438 | 439 | /** 440 | * @param string $table 441 | * @param array $columns 442 | * @return void 443 | */ 444 | public function sqlDropColumns($table, $columns) 445 | { 446 | DB::query("ALTER TABLE \"$table\" DROP \"" . implode('", DROP "', $columns) . "\""); 447 | } 448 | 449 | /** 450 | * @param string $table 451 | * @param array $columns 452 | * @return void 453 | */ 454 | public function sqlLiteDropColumns($table, $columns) 455 | { 456 | $newColsSpec = $newCols = []; 457 | foreach (DataObject::getSchema()->databaseFields($table) as $name => $spec) { 458 | if (in_array($name, $columns)) { 459 | continue; 460 | } 461 | $newColsSpec[] = "\"$name\" $spec"; 462 | $newCols[] = "\"$name\""; 463 | } 464 | 465 | $queries = [ 466 | "BEGIN TRANSACTION", 467 | "CREATE TABLE \"{$table}_cleanup\" (" . implode(',', $newColsSpec) . ")", 468 | "INSERT INTO \"{$table}_cleanup\" SELECT " . implode(',', $newCols) . " FROM \"$table\"", 469 | "DROP TABLE \"$table\"", 470 | "ALTER TABLE \"{$table}_cleanup\" RENAME TO \"{$table}\"", 471 | "COMMIT" 472 | ]; 473 | 474 | foreach ($queries as $query) { 475 | DB::query($query . ';'); 476 | } 477 | } 478 | } 479 | -------------------------------------------------------------------------------- /src/Tasks/FakeRecordGeneratorTask.php: -------------------------------------------------------------------------------- 1 | request = $request; 32 | 33 | $list = $this->getValidDataObjects(); 34 | $this->addOption("model", "Which model to generate", null, $list); 35 | $this->addOption("how_many", "How many records to generate", 20); 36 | $this->addOption("member_from_api", "Use https://randomuser.me to generate members", true); 37 | $this->addOption("clear_existing", "Clear existing data", false); 38 | 39 | $options = $this->askOptions(); 40 | 41 | $model = $options['model']; 42 | $how_many = $options['how_many']; 43 | $member_from_api = $options['member_from_api']; 44 | $clear_existing = $options['clear_existing']; 45 | 46 | if ($model) { 47 | $sing = singleton($model); 48 | 49 | if ($clear_existing) { 50 | $this->message("Clearing existing data", "warning"); 51 | $cleared = 0; 52 | foreach ($model::get() as $rec) { 53 | $rec->delete(); 54 | $cleared++; 55 | } 56 | $this->message("Cleared $cleared records"); 57 | } 58 | 59 | if ($model == Member::class && $member_from_api) { 60 | $this->createMembersFromApi($how_many); 61 | } else { 62 | for ($i = 0; $i < $how_many; $i++) { 63 | $this->message("Generating record $i"); 64 | 65 | try { 66 | $rec = $model::create(); 67 | 68 | $fields = $rec->getCMSFields(); 69 | 70 | // Fill according to type 71 | $db = $model::config()->db; 72 | $owns = $model::config()->owns; 73 | $has_one = $model::config()->has_one; 74 | $has_many = $model::config()->has_many; 75 | $many_many = $model::config()->many_many; 76 | 77 | foreach ($db as $name => $type) { 78 | $rec->$name = $this->getRandomValueFromType($type, $name, $rec); 79 | 80 | $field = $fields->dataFieldByName($name); 81 | if (!$field) { 82 | continue; 83 | } 84 | 85 | // For dropdown fields and the likes, we might use the getSource thing 86 | if ($field->hasMethod('getSource')) { 87 | $source = $field->getSource(); 88 | if (is_array($source)) { 89 | $source = array_keys($source); 90 | $value = $source[array_rand($source)]; 91 | 92 | // Use save into to ensure consistency 93 | $field->setValue($value); 94 | $field->saveInto($rec); 95 | } 96 | } 97 | } 98 | 99 | $hasFillFake = $rec->hasMethod('fillFake'); 100 | 101 | // Only populate relations for record without fillFake 102 | if (!$hasFillFake) { 103 | foreach ($has_one as $name => $class) { 104 | $rel = null; 105 | $nameID = $name . 'ID'; 106 | $isOwned = isset($owns[$name]) ? true : false; 107 | 108 | if ($isOwned) { 109 | if ($class == Image::class) { 110 | $rel = FakeDataProvider::ownImage($rec, $name); 111 | } 112 | } else { 113 | if ($class == Image::class) { 114 | $rel = FakeDataProvider::image(); 115 | } else { 116 | $rel = FakeDataProvider::record($class); 117 | } 118 | } 119 | 120 | if ($rel) { 121 | $rec->$nameID = $rel->ID; 122 | } 123 | } 124 | foreach ($many_many as $name => $class) { 125 | if (is_array($class)) { 126 | continue; 127 | } 128 | 129 | $rel = null; 130 | if ($isOwned) { 131 | if ($class == Image::class) { 132 | $rel = FakeDataProvider::ownImage($rec); 133 | } 134 | } else { 135 | $rel = FakeDataProvider::record($class); 136 | } 137 | if ($rel) { 138 | $rec->$name()->add($rel->ID); 139 | } 140 | } 141 | } 142 | 143 | $id = $rec->write(); 144 | 145 | if ($hasFillFake) { 146 | $rec->fillFake(); 147 | } 148 | 149 | $id = $rec->write(); 150 | 151 | $this->message("New record with id $id", "created"); 152 | } catch (Exception $ex) { 153 | $this->message($ex->getMessage(), "error"); 154 | } 155 | } 156 | } 157 | } else { 158 | $this->message("Implement 'fillFake' method to create your own fakes"); 159 | } 160 | } 161 | 162 | protected function getRandomValueFromType($type, $name, $record) 163 | { 164 | $type = explode('(', $type); 165 | switch ($type[0]) { 166 | case 'Varchar': 167 | case DBVarchar::class: 168 | $length = 50; 169 | if (count($type) > 1) { 170 | $length = (int) $type[1]; 171 | } 172 | if ($name == 'CountryCode' || $name == 'Nationality') { 173 | return FakeDataProvider::countryCode(); 174 | } elseif ($name == 'PostalCode' || $name == 'Postcode') { 175 | $addr = FakeDataProvider::address(); 176 | return $addr['Postcode']; 177 | } elseif ($name == 'Locality' || $name == 'City') { 178 | $addr = FakeDataProvider::address(); 179 | return $addr['City']; 180 | } elseif ($name == 'URLSegment' || $name == 'Slug') { 181 | return null; // let autogeneration happen 182 | } 183 | return FakeDataProvider::words(3, 7); 184 | case 'Date': 185 | case 'DateTime': 186 | case DBDate::class: 187 | return FakeDataProvider::date(strtotime('-1 year'), strtotime('+1 year')); 188 | case 'Boolean': 189 | case DBBoolean::class: 190 | return FakeDataProvider::boolean(); 191 | case 'Enum': 192 | case 'NiceEnum': 193 | case DBEnum::class: 194 | /* @var $enum Enum */ 195 | $enum = $record->dbObject($name); 196 | return FakeDataProvider::pick(array_values($enum->enumValues())); 197 | case 'Int': 198 | case DBInt::class: 199 | return rand(1, 10); 200 | case 'Currency': 201 | case DBCurrency::class: 202 | return FakeDataProvider::fprand(20, 100, 2); 203 | case 'HTMLText': 204 | case DBHTMLText::class: 205 | return FakeDataProvider::paragraphs(3, 7); 206 | case 'Text': 207 | case DBText::class: 208 | return FakeDataProvider::sentences(3, 7); 209 | default: 210 | $dbObject = $record->dbObject($name); 211 | if ($dbObject && $dbObject->hasMethod('fillFake')) { 212 | return $dbObject->fillFake(); 213 | } 214 | return null; 215 | } 216 | } 217 | 218 | protected function createMembersFromApi($how_many) 219 | { 220 | $data = FakeDataProvider::randomUser(['result' => $how_many]); 221 | foreach ($data as $res) { 222 | try { 223 | $rec = Member::create(); 224 | $rec->Gender = $res['gender']; 225 | $rec->FirstName = ucwords($res['name']['first']); 226 | $rec->Surname = ucwords($res['name']['last']); 227 | $rec->Salutation = ucwords($res['name']['title']); 228 | $rec->Address = $res['location']['street']; 229 | $rec->Locality = $res['location']['city']; 230 | $rec->PostalCode = $res['location']['postcode']; 231 | $rec->BirthDate = $res['dob']; 232 | $rec->Created = $res['registered']; 233 | $rec->Phone = $res['phone']; 234 | $rec->Cell = $res['cell']; 235 | $rec->Nationality = $res['nat']; 236 | $rec->Email = $res['email']; 237 | 238 | $image_data = file_get_contents($res['picture']['large']); 239 | $image = FakeDataProvider::storeFakeImage($image_data, basename($res['picture']['large']), 'Avatars'); 240 | $rec->AvatarID = $image->ID; 241 | 242 | $id = $rec->write(); 243 | 244 | $rec->changePassword($res['login']['password']); 245 | 246 | if ($rec->hasMethod('fillFake')) { 247 | $rec->fillFake(); 248 | } 249 | $id = $rec->write(); 250 | 251 | $this->message("New record with id $id", "created"); 252 | } catch (Exception $ex) { 253 | $this->message($ex->getMessage(), "error"); 254 | } 255 | } 256 | } 257 | 258 | public function isEnabled() 259 | { 260 | return Director::isDev(); 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /src/Tasks/PhpInfoTask.php: -------------------------------------------------------------------------------- 1 | request = $request; 41 | 42 | set_time_limit(0); 43 | SubsiteHelper::disableFilter(); 44 | $admin = self::getAssetAdmin(); 45 | 46 | $originalDir = BASE_PATH . '/' . Director::publicDir() . '/assets/'; 47 | 48 | $files = File::get(); 49 | 50 | $this->message("Processing {$files->count()} files"); 51 | 52 | $i = 0; 53 | foreach ($files as $file) { 54 | $i++; 55 | $name = $file->getFilename(); 56 | 57 | if (!$name) { 58 | continue; 59 | } 60 | 61 | $originalName = $originalDir . $name; 62 | 63 | // Generate a file hash if not set 64 | if (!$file->getField('FileHash') && is_file($originalName)) { 65 | $hash = sha1_file($originalName); 66 | DB::query('UPDATE "File" SET "FileHash" = \'' . $hash . '\' WHERE "ID" = \'' . $file->ID . '\' LIMIT 1;'); 67 | $targetDir = str_replace('./', '', BASE_PATH . '/' . Director::publicDir() . '/assets/.protected/' . dirname($name) 68 | . '/' . substr($hash, 0, 10) . '/'); 69 | if (!file_exists($targetDir)) { 70 | mkdir($targetDir, 0755, true); 71 | } 72 | rename($originalDir . $name, $targetDir . basename($name)); 73 | echo '' . $originalDir . $name . ' > ' . $targetDir . basename($name) . '
    '; 74 | } else { 75 | // Will only apply to images 76 | $admin->generateThumbnails($file); 77 | // Publish 78 | try { 79 | $file->copyVersionToStage('Stage', 'Live'); 80 | $this->message("Published $name", "created"); 81 | } catch (Exception $ex) { 82 | $this->message($ex->getMessage(), "error"); 83 | } 84 | } 85 | 86 | $file->destroy(); 87 | } 88 | $this->message("Processed $i files"); 89 | } 90 | 91 | public function isEnabled() 92 | { 93 | return Director::isDev(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Tasks/RehashImagesTask.php: -------------------------------------------------------------------------------- 1 | request = $request; 28 | $this->addOption("go", "Set this to 1 to proceed", 0); 29 | 30 | $options = $this->askOptions(); 31 | 32 | $go = $options['go']; 33 | 34 | $service = new Sha1FileHashingService(); 35 | 36 | $images = Image::get(); 37 | $tofix = $todelete = 0; 38 | foreach ($images as $image) { 39 | $hash = $image->getHash(); 40 | 41 | $stream = $image->getStream(); 42 | if (!$stream) { 43 | $filename = $image->getFilename(); 44 | $location = ASSETS_PATH . '/' . $filename; 45 | if (!is_file($location)) { 46 | $todelete++; 47 | if ($go) { 48 | $this->message("Deleted file " . $image->ID . " since the file was not found"); 49 | $image->delete(); 50 | } else { 51 | $this->message("Could not read file at $location. It would be deleted by this task.", "bad"); 52 | } 53 | continue; 54 | } 55 | $stream = fopen($location, 'rb'); 56 | } 57 | 58 | $fullhash = $service->computeFromStream($stream); 59 | 60 | if ($hash != $fullhash) { 61 | $tofix++; 62 | if ($go) { 63 | DB::query("UPDATE File SET FileHash = '" . $fullhash . "' WHERE ID = " . $image->ID); 64 | DB::query("UPDATE File_Live SET FileHash = '" . $fullhash . "' WHERE ID = " . $image->ID); 65 | $this->message($image->ID . " has been fixed"); 66 | } else { 67 | $this->message($image->ID . " hash mismatch : $hash vs $fullhash. This task can fix this hash."); 68 | } 69 | } 70 | } 71 | 72 | if (!$tofix && !$todelete) { 73 | $this->message("All files are good", "good"); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Tasks/RemoveEmptyGroupsTask.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class RemoveEmptyGroupsTask extends BuildTask 15 | { 16 | use BuildTaskTools; 17 | 18 | protected $title = "Remove empty/duplicate groups from the cms"; 19 | private static $segment = 'RemoveEmptyGroupsTask'; 20 | 21 | public function run($request) 22 | { 23 | $this->request = $request; 24 | $groups = Group::get(); 25 | echo 'Pass ?drop=1 to drop groups without members
    '; 26 | echo 'Want more dropping? Pass ?permission=1 to also drop groups without permissions even if they have members
    '; 27 | echo 'Pass ?merge=1 to merge groups with the same code
    '; 28 | echo 'Want to merge across subsites ? Pass ?subsite=1 to disable subsite filters
    '; 29 | 30 | echo '
    '; 31 | $merge = $request->getVar('merge'); 32 | $drop = $request->getVar('drop'); 33 | $dropNoPermission = $request->getVar('permission'); 34 | $subsite = $request->getVar('subsite'); 35 | 36 | if (class_exists(Subsite::class) && $subsite) { 37 | Subsite::$disable_subsite_filter = true; 38 | } 39 | 40 | if ($drop) { 41 | DB::alteration_message("Dropping groups with no members"); 42 | if ($dropNoPermission) { 43 | DB::alteration_message("Also dropping groups with no permissions"); 44 | } 45 | foreach ($groups as $group) { 46 | if (!$group->Members()->count()) { 47 | DB::alteration_message( 48 | "Removing group {$group->ID} because it has no members", 49 | "deleted" 50 | ); 51 | $group->delete(); 52 | } 53 | if ($dropNoPermission) { 54 | $c = $group->Permissions()->count(); 55 | if (!$c) { 56 | DB::alteration_message( 57 | "Removing group {$group->ID} because it has no permissions", 58 | "deleted" 59 | ); 60 | $group->delete(); 61 | } 62 | } 63 | } 64 | } 65 | if ($merge) { 66 | DB::alteration_message("Merging groups with duplicated codes"); 67 | $index = array(); 68 | 69 | /* @var $group Group */ 70 | foreach ($groups as $group) { 71 | DB::alteration_message("Found group " . $group->Code); 72 | if (!isset($index[$group->Code])) { 73 | $index[$group->Code] = $group; 74 | DB::alteration_message("First instance of group, do not merge"); 75 | continue; 76 | } 77 | 78 | $mergeGroup = $index[$group->Code]; 79 | 80 | 81 | DB::alteration_message( 82 | 'Merge group ' . $group->ID . ' with ' . $mergeGroup->ID, 83 | 'repaired' 84 | ); 85 | 86 | $i = 0; 87 | foreach ($group->Members() as $m) { 88 | $i++; 89 | $mergeGroup->Members()->add($m); 90 | } 91 | DB::alteration_message( 92 | 'Added ' . $i . ' members to group', 93 | 'created' 94 | ); 95 | 96 | DB::alteration_message( 97 | "Group " . $group->ID . ' was deleted', 98 | 'deleted' 99 | ); 100 | $group->delete(); 101 | } 102 | } 103 | 104 | DB::alteration_message('All done!'); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Tasks/RemoveOldPermissionsTask.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class RemoveOldPermissionsTask extends BuildTask 16 | { 17 | use BuildTaskTools; 18 | 19 | protected $title = "Remove 'other' permissions from the cms"; 20 | private static $segment = 'RemoveOldPermissionsTask'; 21 | 22 | public function run($request) 23 | { 24 | $this->request = $request; 25 | 26 | $permissions = Permission::get_codes(true); 27 | 28 | $other = $permissions['Other']; 29 | foreach ($other as $k => $infos) { 30 | DB::prepared_query("DELETE FROM Permission WHERE Code = ?", [$k]); 31 | DB::alteration_message("Deleting $k"); 32 | } 33 | 34 | DB::alteration_message('All done!'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Tasks/SitePublisherTask.php: -------------------------------------------------------------------------------- 1 | request = $request; 22 | $this->addOption("classes", "Classes to publish (comma separated)", 'Page'); 23 | $this->addOption("go", "Set this to 1 to proceed", 0); 24 | 25 | $options = $this->askOptions(); 26 | 27 | $classes = $options['classes']; 28 | $go = $options['go']; 29 | 30 | $cbSave = function ($List, $publish = false) { 31 | foreach ($List as $Item) { 32 | $this->message('Saving item "' . $Item->getTitle() . '"'); 33 | if ($publish) { 34 | $Item->publishRecursive(); 35 | } else { 36 | $Item->write(); 37 | } 38 | } 39 | }; 40 | 41 | $classesToPublish = explode(',', $classes); 42 | 43 | if ($go) { 44 | foreach ($classesToPublish as $class) { 45 | $this->message("Publishing class $class"); 46 | 47 | $List = $class::get(); 48 | $singl = $class::singleton(); 49 | $fluent = $singl->has_extension("\\TractorCow\\Fluent\\Extension\\FluentExtension"); 50 | 51 | $publish = $singl->has_extension(Versioned::class); 52 | 53 | // With fluent we need to change state when saving 54 | if ($fluent) { 55 | $state = \TractorCow\Fluent\State\FluentState::singleton(); 56 | $allLocales = \TractorCow\Fluent\Model\Locale::get(); 57 | foreach ($allLocales as $locale) { 58 | $this->message('Publishing with locale ' . $locale->Locale, "info"); 59 | $state->withState(function ($state) use ($locale, $List, $cbSave, $publish) { 60 | $state->setLocale($locale->Locale); 61 | $cbSave($List, $publish); 62 | }); 63 | } 64 | } else { 65 | $cbSave($List, $publish); 66 | } 67 | } 68 | } else { 69 | foreach ($classesToPublish as $class) { 70 | $this->message("Would publish " . $class::get()->count() . " items for class " . $class); 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Tasks/TestCacheSupportTask.php: -------------------------------------------------------------------------------- 1 | request = $request; 26 | 27 | $this->testMemcache(); 28 | $this->testOpcache(); 29 | $this->testRedis(); 30 | } 31 | 32 | protected function testRedis() 33 | { 34 | $predis = new Client('tcp://127.0.0.1:6379'); 35 | $this->message($predis->executeCommand(new ServerInfo)); 36 | 37 | $args = []; 38 | $redisCache = Injector::inst()->createWithArgs(RedisCacheFactory::class, $args); 39 | $this->message($redisCache); 40 | } 41 | 42 | protected function testOpcache() 43 | { 44 | if (!function_exists('opcache_get_status')) { 45 | $this->msg("opcache_get_status function is not defined"); 46 | } 47 | 48 | $result = opcache_get_status(); 49 | if ($result) { 50 | $this->msg("Opcache is active"); 51 | 52 | echo '
    ';
    53 |             print_r($result);
    54 |             echo '
    '; 55 | } else { 56 | $this->msg("Opcache is disabled. It should be enabled to ensure optimal performances", "error"); 57 | } 58 | } 59 | protected function testMemcache() 60 | { 61 | if (!class_exists('Memcache')) { 62 | $this->msg("Memcache class does not exist. Make sure that the Memcache extension is installed"); 63 | } 64 | 65 | $host = defined('MEMCACHE_HOST') ? MEMCACHE_HOST : 'localhost'; 66 | $port = defined('MEMCACHE_PORT') ? MEMCACHE_PORT : 11211; 67 | 68 | $memcache = new \Memcache; 69 | $connected = $memcache->connect($host, $port); 70 | 71 | if ($connected) { 72 | $this->msg("Server's version: " . $memcache->getVersion()); 73 | 74 | $result = $memcache->get("key"); 75 | 76 | if ($result) { 77 | $this->msg("Data found in cache"); 78 | } else { 79 | $this->msg("Data not found in cache"); 80 | $tmp_object = new stdClass; 81 | $tmp_object->str_attr = "test"; 82 | $tmp_object->int_attr = 123; 83 | $tmp_object->time = time(); 84 | $tmp_object->date = date('Y-m-d H:i:s'); 85 | $tmp_object->arr = array(1, 2, 3); 86 | $memcache->set("key", $tmp_object, false, 10); 87 | } 88 | 89 | $this->msg("Store data in the cache (data will expire in 10 seconds)"); 90 | $this->msg("Data from the cache:"); 91 | echo '
    ';
    92 |             var_dump($memcache->get("key"));
    93 |             echo '
    '; 94 | } else { 95 | $this->msg("Failed to connect"); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/TypographyController.php: -------------------------------------------------------------------------------- 1 | Title = 'Typography test page'; 38 | $this->ExtraMeta .= ''; 39 | 40 | return $this->renderWith(array('Typography', 'Page')); 41 | } 42 | public function RandomImage() 43 | { 44 | return Image::get()->sort('RAND()')->first(); 45 | } 46 | public function TypoForm() 47 | { 48 | $array = array('green', 'yellow', 'blue', 'pink', 'orange'); 49 | $form = new Form( 50 | $this, 51 | 'TestForm', 52 | $fields = FieldList::create( 53 | HeaderField::create('HeaderField1', 'HeaderField Level 1', 1), 54 | LiteralField::create('LiteralField', '

    All fields up to EmailField are required and should be marked as such

    '), 55 | TextField::create('TextField1', 'Text Field Example 1'), 56 | TextField::create('TextField2', 'Text Field Example 2'), 57 | TextField::create('TextField3', 'Text Field Example 3'), 58 | TextField::create('TextField4', ''), 59 | HeaderField::create('FieldGroupHdr', 'First/last name FieldGroup'), 60 | FieldGroup::create( 61 | TextField::create('FirstName1', 'First Name'), 62 | TextField::create('LastName1', 'Last Name') 63 | ), 64 | HeaderField::create('HeaderField2b', 'Field with right title', 2), 65 | TextareaField::create('TextareaField', 'Textarea Field') 66 | ->setColumns(45) 67 | ->setRightTitle('This is the right title'), 68 | EmailField::create('EmailField', 'Email address'), 69 | HeaderField::create('HeaderField2c', 'HeaderField Level 2', 2), 70 | DropdownField::create('DropdownField', 'Dropdown Field', array(0 => '-- please select --', 1 => 'test AAAA', 2 => 'test BBBB')), 71 | OptionsetField::create('OptionSF', 'Optionset Field', $array), 72 | CheckboxSetField::create('CheckboxSF', 'Checkbox Set Field', $array), 73 | CurrencyField::create('CurrencyField', 'Bling bling', '$123.45'), 74 | HeaderField::create('HeaderField3', 'Other Fields', 3), 75 | NumericField::create('NumericField', 'Numeric Field '), 76 | DateField::create('DateField', 'Date Field'), 77 | DateTimeField::create('DateTimeField', 'Date and Time Field'), 78 | CheckboxField::create('CheckboxField', 'Checkbox Field') 79 | ), 80 | $actions = FieldList::create( 81 | FormAction::create('submit', 'Submit Button') 82 | ), 83 | $requiredFields = RequiredFields::create( 84 | 'TextField1', 85 | 'TextField2', 86 | 'TextField3', 87 | 'ErrorField1', 88 | 'ErrorField2', 89 | 'EmailField', 90 | 'TextField3', 91 | 'RightTitleField', 92 | 'CheckboxField', 93 | 'CheckboxSetField' 94 | ) 95 | ); 96 | $form->setMessage('warning message', 'warning'); 97 | return $form; 98 | } 99 | public function TestForm($data) 100 | { 101 | $this->redirectBack(); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /templates/Layout/Typography.ss: -------------------------------------------------------------------------------- 1 |
    2 | <% include TypographySampleText %> 3 |
    4 | -------------------------------------------------------------------------------- /tests/DevToolkitTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 17 | } 18 | } 19 | --------------------------------------------------------------------------------