diff --git a/php/domain-validator.php b/php/domain-validator.php index 57506b8a1c1..e7e5caa7347 100644 --- a/php/domain-validator.php +++ b/php/domain-validator.php @@ -2,18 +2,18 @@ $domain = $_GET['domain'] ?? ''; -if (!str_contains($domain, '.')) { - http_response_code(400); -} elseif (str_contains($domain, '/')) { - http_response_code(400); -} elseif (str_contains($domain, ':')) { - http_response_code(400); -} elseif (filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) === false) { - http_response_code(400); -} elseif (filter_var($domain, FILTER_VALIDATE_IP)) { - http_response_code(400); +if (!str_contains($domain, '.')) { + http_response_code(400); +} elseif (str_contains($domain, '/')) { + http_response_code(400); +} elseif (str_contains($domain, ':')) { + http_response_code(400); +} elseif (filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) === false) { + http_response_code(400); +} elseif (filter_var($domain, FILTER_VALIDATE_IP)) { + http_response_code(400); } else { - // Commented because logging is disabled as otherwise all attempts will be logged which spams the logs - // error_log($domain . ' was accepted as valid domain.'); - http_response_code(200); + // Commented because logging is disabled as otherwise all attempts will be logged which spams the logs + // error_log($domain . ' was accepted as valid domain.'); + http_response_code(200); } diff --git a/php/src/Auth/AuthManager.php b/php/src/Auth/AuthManager.php index a4bc96b6d3c..51ee54eb0fa 100644 --- a/php/src/Auth/AuthManager.php +++ b/php/src/Auth/AuthManager.php @@ -4,43 +4,43 @@ use AIO\Data\ConfigurationManager; use AIO\Data\DataConst; -use \DateTime; +use DateTime; class AuthManager { - private const string SESSION_KEY = 'aio_authenticated'; - private ConfigurationManager $configurationManager; + private const string SESSION_KEY = 'aio_authenticated'; + private ConfigurationManager $configurationManager; - public function __construct(ConfigurationManager $configurationManager) { - $this->configurationManager = $configurationManager; - } + public function __construct(ConfigurationManager $configurationManager) { + $this->configurationManager = $configurationManager; + } - public function CheckCredentials(string $password) : bool { - return hash_equals($this->configurationManager->GetPassword(), $password); - } + public function CheckCredentials(string $password) : bool { + return hash_equals($this->configurationManager->GetPassword(), $password); + } - public function CheckToken(string $token) : bool { - return hash_equals($this->configurationManager->GetToken(), $token); - } + public function CheckToken(string $token) : bool { + return hash_equals($this->configurationManager->GetToken(), $token); + } - public function SetAuthState(bool $isLoggedIn) : void { + public function SetAuthState(bool $isLoggedIn) : void { - if (!$this->IsAuthenticated() && $isLoggedIn === true) { - $date = new DateTime(); - $dateTime = $date->getTimestamp(); - $_SESSION['date_time'] = $dateTime; + if (!$this->IsAuthenticated() && $isLoggedIn === true) { + $date = new DateTime(); + $dateTime = $date->getTimestamp(); + $_SESSION['date_time'] = $dateTime; - $df = disk_free_space(DataConst::GetSessionDirectory()); - if ($df !== false && (int)$df < 10240) { - error_log(DataConst::GetSessionDirectory() . " has only less than 10KB free space. The login might not succeed because of that!"); - } + $df = disk_free_space(DataConst::GetSessionDirectory()); + if ($df !== false && (int)$df < 10240) { + error_log(DataConst::GetSessionDirectory() . ' has only less than 10KB free space. The login might not succeed because of that!'); + } - file_put_contents(DataConst::GetSessionDateFile(), (string)$dateTime); - } + file_put_contents(DataConst::GetSessionDateFile(), (string)$dateTime); + } - $_SESSION[self::SESSION_KEY] = $isLoggedIn; - } + $_SESSION[self::SESSION_KEY] = $isLoggedIn; + } - public function IsAuthenticated() : bool { - return isset($_SESSION[self::SESSION_KEY]) && $_SESSION[self::SESSION_KEY] === true; - } + public function IsAuthenticated() : bool { + return isset($_SESSION[self::SESSION_KEY]) && $_SESSION[self::SESSION_KEY] === true; + } } diff --git a/php/src/Auth/PasswordGenerator.php b/php/src/Auth/PasswordGenerator.php index 6e9ee9a182a..7e7d423c51d 100644 --- a/php/src/Auth/PasswordGenerator.php +++ b/php/src/Auth/PasswordGenerator.php @@ -2,7800 +2,7796 @@ namespace AIO\Auth; -use AIO\Data\ConfigurationManager; +class PasswordGenerator { + private array $words = [ + 'abacus', + 'abdomen', + 'abdominal', + 'abide', + 'abiding', + 'ability', + 'ablaze', + 'able', + 'abnormal', + 'abrasion', + 'abrasive', + 'abreast', + 'abridge', + 'abroad', + 'abruptly', + 'absence', + 'absentee', + 'absently', + 'absinthe', + 'absolute', + 'absolve', + 'abstain', + 'abstract', + 'absurd', + 'accent', + 'acclaim', + 'acclimate', + 'accompany', + 'account', + 'accuracy', + 'accurate', + 'accustom', + 'acetone', + 'achiness', + 'aching', + 'acid', + 'acorn', + 'acquaint', + 'acquire', + 'acre', + 'acrobat', + 'acronym', + 'acting', + 'action', + 'activate', + 'activator', + 'active', + 'activism', + 'activist', + 'activity', + 'actress', + 'acts', + 'acutely', + 'acuteness', + 'aeration', + 'aerobics', + 'aerosol', + 'aerospace', + 'afar', + 'affair', + 'affected', + 'affecting', + 'affection', + 'affidavit', + 'affiliate', + 'affirm', + 'affix', + 'afflicted', + 'affluent', + 'afford', + 'affront', + 'aflame', + 'afloat', + 'aflutter', + 'afoot', + 'afraid', + 'afterglow', + 'afterlife', + 'aftermath', + 'aftermost', + 'afternoon', + 'aged', + 'ageless', + 'agency', + 'agenda', + 'agent', + 'aggregate', + 'aghast', + 'agile', + 'agility', + 'aging', + 'agnostic', + 'agonize', + 'agonizing', + 'agony', + 'agreeable', + 'agreeably', + 'agreed', + 'agreeing', + 'agreement', + 'aground', + 'ahead', + 'ahoy', + 'aide', + 'aids', + 'aim', + 'ajar', + 'alabaster', + 'alarm', + 'albatross', + 'album', + 'alfalfa', + 'algebra', + 'algorithm', + 'alias', + 'alibi', + 'alienable', + 'alienate', + 'aliens', + 'alike', + 'alive', + 'alkaline', + 'alkalize', + 'almanac', + 'almighty', + 'almost', + 'aloe', + 'aloft', + 'aloha', + 'alone', + 'alongside', + 'aloof', + 'alphabet', + 'alright', + 'although', + 'altitude', + 'alto', + 'aluminum', + 'alumni', + 'always', + 'amaretto', + 'amaze', + 'amazingly', + 'amber', + 'ambiance', + 'ambiguity', + 'ambiguous', + 'ambition', + 'ambitious', + 'ambulance', + 'ambush', + 'amendable', + 'amendment', + 'amends', + 'amenity', + 'amiable', + 'amicably', + 'amid', + 'amigo', + 'amino', + 'amiss', + 'ammonia', + 'ammonium', + 'amnesty', + 'amniotic', + 'among', + 'amount', + 'amperage', + 'ample', + 'amplifier', + 'amplify', + 'amply', + 'amuck', + 'amulet', + 'amusable', + 'amused', + 'amusement', + 'amuser', + 'amusing', + 'anaconda', + 'anaerobic', + 'anagram', + 'anatomist', + 'anatomy', + 'anchor', + 'anchovy', + 'ancient', + 'android', + 'anemia', + 'anemic', + 'aneurism', + 'anew', + 'angelfish', + 'angelic', + 'anger', + 'angled', + 'angler', + 'angles', + 'angling', + 'angrily', + 'angriness', + 'anguished', + 'angular', + 'animal', + 'animate', + 'animating', + 'animation', + 'animator', + 'anime', + 'animosity', + 'ankle', + 'annex', + 'annotate', + 'announcer', + 'annoying', + 'annually', + 'annuity', + 'anointer', + 'another', + 'answering', + 'antacid', + 'antarctic', + 'anteater', + 'antelope', + 'antennae', + 'anthem', + 'anthill', + 'anthology', + 'antibody', + 'antics', + 'antidote', + 'antihero', + 'antiquely', + 'antiques', + 'antiquity', + 'antirust', + 'antitoxic', + 'antitrust', + 'antiviral', + 'antivirus', + 'antler', + 'antonym', + 'antsy', + 'anvil', + 'anybody', + 'anyhow', + 'anymore', + 'anyone', + 'anyplace', + 'anything', + 'anytime', + 'anyway', + 'anywhere', + 'aorta', + 'apache', + 'apostle', + 'appealing', + 'appear', + 'appease', + 'appeasing', + 'appendage', + 'appendix', + 'appetite', + 'appetizer', + 'applaud', + 'applause', + 'apple', + 'appliance', + 'applicant', + 'applied', + 'apply', + 'appointee', + 'appraisal', + 'appraiser', + 'apprehend', + 'approach', + 'approval', + 'approve', + 'apricot', + 'april', + 'apron', + 'aptitude', + 'aptly', + 'aqua', + 'aqueduct', + 'arbitrary', + 'arbitrate', + 'ardently', + 'area', + 'arena', + 'arguable', + 'arguably', + 'argue', + 'arise', + 'armadillo', + 'armband', + 'armchair', + 'armed', + 'armful', + 'armhole', + 'arming', + 'armless', + 'armoire', + 'armored', + 'armory', + 'armrest', + 'army', + 'aroma', + 'arose', + 'around', + 'arousal', + 'arrange', + 'array', + 'arrest', + 'arrival', + 'arrive', + 'arrogance', + 'arrogant', + 'arson', + 'art', + 'ascend', + 'ascension', + 'ascent', + 'ascertain', + 'ashamed', + 'ashen', + 'ashes', + 'ashy', + 'aside', + 'askew', + 'asleep', + 'asparagus', + 'aspect', + 'aspirate', + 'aspire', + 'aspirin', + 'astonish', + 'astound', + 'astride', + 'astrology', + 'astronaut', + 'astronomy', + 'astute', + 'atlantic', + 'atlas', + 'atom', + 'atonable', + 'atop', + 'atrium', + 'atrocious', + 'atrophy', + 'attach', + 'attain', + 'attempt', + 'attendant', + 'attendee', + 'attention', + 'attentive', + 'attest', + 'attic', + 'attire', + 'attitude', + 'attractor', + 'attribute', + 'atypical', + 'auction', + 'audacious', + 'audacity', + 'audible', + 'audibly', + 'audience', + 'audio', + 'audition', + 'augmented', + 'august', + 'authentic', + 'author', + 'autism', + 'autistic', + 'autograph', + 'automaker', + 'automated', + 'automatic', + 'autopilot', + 'available', + 'avalanche', + 'avatar', + 'avenge', + 'avenging', + 'avenue', + 'average', + 'aversion', + 'avert', + 'aviation', + 'aviator', + 'avid', + 'avoid', + 'await', + 'awaken', + 'award', + 'aware', + 'awhile', + 'awkward', + 'awning', + 'awoke', + 'awry', + 'axis', + 'babble', + 'babbling', + 'babied', + 'baboon', + 'backache', + 'backboard', + 'backboned', + 'backdrop', + 'backed', + 'backer', + 'backfield', + 'backfire', + 'backhand', + 'backing', + 'backlands', + 'backlash', + 'backless', + 'backlight', + 'backlit', + 'backlog', + 'backpack', + 'backpedal', + 'backrest', + 'backroom', + 'backshift', + 'backside', + 'backslid', + 'backspace', + 'backspin', + 'backstab', + 'backstage', + 'backtalk', + 'backtrack', + 'backup', + 'backward', + 'backwash', + 'backwater', + 'backyard', + 'bacon', + 'bacteria', + 'bacterium', + 'badass', + 'badge', + 'badland', + 'badly', + 'badness', + 'baffle', + 'baffling', + 'bagel', + 'bagful', + 'baggage', + 'bagged', + 'baggie', + 'bagginess', + 'bagging', + 'baggy', + 'bagpipe', + 'baguette', + 'baked', + 'bakery', + 'bakeshop', + 'baking', + 'balance', + 'balancing', + 'balcony', + 'balmy', + 'balsamic', + 'bamboo', + 'banana', + 'banish', + 'banister', + 'banjo', + 'bankable', + 'bankbook', + 'banked', + 'banker', + 'banking', + 'banknote', + 'bankroll', + 'banner', + 'bannister', + 'banshee', + 'banter', + 'barbecue', + 'barbed', + 'barbell', + 'barber', + 'barcode', + 'barge', + 'bargraph', + 'barista', + 'baritone', + 'barley', + 'barmaid', + 'barman', + 'barn', + 'barometer', + 'barrack', + 'barracuda', + 'barrel', + 'barrette', + 'barricade', + 'barrier', + 'barstool', + 'bartender', + 'barterer', + 'bash', + 'basically', + 'basics', + 'basil', + 'basin', + 'basis', + 'basket', + 'batboy', + 'batch', + 'bath', + 'baton', + 'bats', + 'battalion', + 'battered', + 'battering', + 'battery', + 'batting', + 'battle', + 'bauble', + 'bazooka', + 'blabber', + 'bladder', + 'blade', + 'blah', + 'blame', + 'blaming', + 'blanching', + 'blandness', + 'blank', + 'blaspheme', + 'blasphemy', + 'blast', + 'blatancy', + 'blatantly', + 'blazer', + 'blazing', + 'bleach', + 'bleak', + 'bleep', + 'blemish', + 'blend', + 'bless', + 'blighted', + 'blimp', + 'bling', + 'blinked', + 'blinker', + 'blinking', + 'blinks', + 'blip', + 'blissful', + 'blitz', + 'blizzard', + 'bloated', + 'bloating', + 'blob', + 'blog', + 'bloomers', + 'blooming', + 'blooper', + 'blot', + 'blouse', + 'blubber', + 'bluff', + 'bluish', + 'blunderer', + 'blunt', + 'blurb', + 'blurred', + 'blurry', + 'blurt', + 'blush', + 'blustery', + 'boaster', + 'boastful', + 'boasting', + 'boat', + 'bobbed', + 'bobbing', + 'bobble', + 'bobcat', + 'bobsled', + 'bobtail', + 'bodacious', + 'body', + 'bogged', + 'boggle', + 'bogus', + 'boil', + 'bok', + 'bolster', + 'bolt', + 'bonanza', + 'bonded', + 'bonding', + 'bondless', + 'boned', + 'bonehead', + 'boneless', + 'bonelike', + 'boney', + 'bonfire', + 'bonnet', + 'bonsai', + 'bonus', + 'bony', + 'boogeyman', + 'boogieman', + 'book', + 'boondocks', + 'booted', + 'booth', + 'bootie', + 'booting', + 'bootlace', + 'bootleg', + 'boots', + 'boozy', + 'borax', + 'boring', + 'borough', + 'borrower', + 'borrowing', + 'boss', + 'botanical', + 'botanist', + 'botany', + 'botch', + 'both', + 'bottle', + 'bottling', + 'bottom', + 'bounce', + 'bouncing', + 'bouncy', + 'bounding', + 'boundless', + 'bountiful', + 'bovine', + 'boxcar', + 'boxer', + 'boxing', + 'boxlike', + 'boxy', + 'breach', + 'breath', + 'breeches', + 'breeching', + 'breeder', + 'breeding', + 'breeze', + 'breezy', + 'brethren', + 'brewery', + 'brewing', + 'briar', + 'bribe', + 'brick', + 'bride', + 'bridged', + 'brigade', + 'bright', + 'brilliant', + 'brim', + 'bring', + 'brink', + 'brisket', + 'briskly', + 'briskness', + 'bristle', + 'brittle', + 'broadband', + 'broadcast', + 'broaden', + 'broadly', + 'broadness', + 'broadside', + 'broadways', + 'broiler', + 'broiling', + 'broken', + 'broker', + 'bronchial', + 'bronco', + 'bronze', + 'bronzing', + 'brook', + 'broom', + 'brought', + 'browbeat', + 'brownnose', + 'browse', + 'browsing', + 'bruising', + 'brunch', + 'brunette', + 'brunt', + 'brush', + 'brussels', + 'brute', + 'brutishly', + 'bubble', + 'bubbling', + 'bubbly', + 'buccaneer', + 'bucked', + 'bucket', + 'buckle', + 'buckshot', + 'buckskin', + 'bucktooth', + 'buckwheat', + 'buddhism', + 'buddhist', + 'budding', + 'buddy', + 'budget', + 'buffalo', + 'buffed', + 'buffer', + 'buffing', + 'buffoon', + 'buggy', + 'bulb', + 'bulge', + 'bulginess', + 'bulgur', + 'bulk', + 'bulldog', + 'bulldozer', + 'bullfight', + 'bullfrog', + 'bullhorn', + 'bullion', + 'bullish', + 'bullpen', + 'bullring', + 'bullseye', + 'bullwhip', + 'bully', + 'bunch', + 'bundle', + 'bungee', + 'bunion', + 'bunkbed', + 'bunkhouse', + 'bunkmate', + 'bunny', + 'bunt', + 'busboy', + 'bush', + 'busily', + 'busload', + 'bust', + 'busybody', + 'buzz', + 'cabana', + 'cabbage', + 'cabbie', + 'cabdriver', + 'cable', + 'caboose', + 'cache', + 'cackle', + 'cacti', + 'cactus', + 'caddie', + 'caddy', + 'cadet', + 'cadillac', + 'cadmium', + 'cage', + 'cahoots', + 'cake', + 'calamari', + 'calamity', + 'calcium', + 'calculate', + 'calculus', + 'caliber', + 'calibrate', + 'calm', + 'caloric', + 'calorie', + 'calzone', + 'camcorder', + 'cameo', + 'camera', + 'camisole', + 'camper', + 'campfire', + 'camping', + 'campsite', + 'campus', + 'canal', + 'canary', + 'cancel', + 'candied', + 'candle', + 'candy', + 'cane', + 'canine', + 'canister', + 'cannabis', + 'canned', + 'canning', + 'cannon', + 'cannot', + 'canola', + 'canon', + 'canopener', + 'canopy', + 'canteen', + 'canyon', + 'capable', + 'capably', + 'capacity', + 'cape', + 'capillary', + 'capital', + 'capitol', + 'capped', + 'capricorn', + 'capsize', + 'capsule', + 'caption', + 'captivate', + 'captive', + 'captivity', + 'capture', + 'caramel', + 'carat', + 'caravan', + 'carbon', + 'cardboard', + 'carded', + 'cardiac', + 'cardigan', + 'cardinal', + 'cardstock', + 'carefully', + 'caregiver', + 'careless', + 'caress', + 'caretaker', + 'cargo', + 'caring', + 'carless', + 'carload', + 'carmaker', + 'carnage', + 'carnation', + 'carnival', + 'carnivore', + 'carol', + 'carpenter', + 'carpentry', + 'carpool', + 'carport', + 'carried', + 'carrot', + 'carrousel', + 'carry', + 'cartel', + 'cartload', + 'carton', + 'cartoon', + 'cartridge', + 'cartwheel', + 'carve', + 'carving', + 'carwash', + 'cascade', + 'case', + 'cash', + 'casing', + 'casino', + 'casket', + 'cassette', + 'casually', + 'casualty', + 'catacomb', + 'catalog', + 'catalyst', + 'catalyze', + 'catapult', + 'cataract', + 'catatonic', + 'catcall', + 'catchable', + 'catcher', + 'catching', + 'catchy', + 'caterer', + 'catering', + 'catfight', + 'catfish', + 'cathedral', + 'cathouse', + 'catlike', + 'catnap', + 'catnip', + 'catsup', + 'cattail', + 'cattishly', + 'cattle', + 'catty', + 'catwalk', + 'caucasian', + 'caucus', + 'causal', + 'causation', + 'cause', + 'causing', + 'cauterize', + 'caution', + 'cautious', + 'cavalier', + 'cavalry', + 'caviar', + 'cavity', + 'cedar', + 'celery', + 'celestial', + 'celibacy', + 'celibate', + 'celtic', + 'cement', + 'census', + 'ceramics', + 'ceremony', + 'certainly', + 'certainty', + 'certified', + 'certify', + 'cesarean', + 'cesspool', + 'chafe', + 'chaffing', + 'chain', + 'chair', + 'chalice', + 'challenge', + 'chamber', + 'chamomile', + 'champion', + 'chance', + 'change', + 'channel', + 'chant', + 'chaos', + 'chaperone', + 'chaplain', + 'chapped', + 'chaps', + 'chapter', + 'character', + 'charbroil', + 'charcoal', + 'charger', + 'charging', + 'chariot', + 'charity', + 'charm', + 'charred', + 'charter', + 'charting', + 'chase', + 'chasing', + 'chaste', + 'chastise', + 'chastity', + 'chatroom', + 'chatter', + 'chatting', + 'chatty', + 'cheating', + 'cheddar', + 'cheek', + 'cheer', + 'cheese', + 'cheesy', + 'chef', + 'chemicals', + 'chemist', + 'chemo', + 'cherisher', + 'cherub', + 'chess', + 'chest', + 'chevron', + 'chevy', + 'chewable', + 'chewer', + 'chewing', + 'chewy', + 'chief', + 'chihuahua', + 'childcare', + 'childhood', + 'childish', + 'childless', + 'childlike', + 'chili', + 'chill', + 'chimp', + 'chip', + 'chirping', + 'chirpy', + 'chitchat', + 'chivalry', + 'chive', + 'chloride', + 'chlorine', + 'choice', + 'chokehold', + 'choking', + 'chomp', + 'chooser', + 'choosing', + 'choosy', + 'chop', + 'chosen', + 'chowder', + 'chowtime', + 'chrome', + 'chubby', + 'chuck', + 'chug', + 'chummy', + 'chump', + 'chunk', + 'churn', + 'chute', + 'cider', + 'cilantro', + 'cinch', + 'cinema', + 'cinnamon', + 'circle', + 'circling', + 'circular', + 'circulate', + 'circus', + 'citable', + 'citadel', + 'citation', + 'citizen', + 'citric', + 'citrus', + 'city', + 'civic', + 'civil', + 'clad', + 'claim', + 'clambake', + 'clammy', + 'clamor', + 'clamp', + 'clamshell', + 'clang', + 'clanking', + 'clapped', + 'clapper', + 'clapping', + 'clarify', + 'clarinet', + 'clarity', + 'clash', + 'clasp', + 'class', + 'clatter', + 'clause', + 'clavicle', + 'claw', + 'clay', + 'clean', + 'clear', + 'cleat', + 'cleaver', + 'cleft', + 'clench', + 'clergyman', + 'clerical', + 'clerk', + 'clever', + 'clicker', + 'client', + 'climate', + 'climatic', + 'cling', + 'clinic', + 'clinking', + 'clip', + 'clique', + 'cloak', + 'clobber', + 'clock', + 'clone', + 'cloning', + 'closable', + 'closure', + 'clothes', + 'clothing', + 'cloud', + 'clover', + 'clubbed', + 'clubbing', + 'clubhouse', + 'clump', + 'clumsily', + 'clumsy', + 'clunky', + 'clustered', + 'clutch', + 'clutter', + 'coach', + 'coagulant', + 'coastal', + 'coaster', + 'coasting', + 'coastland', + 'coastline', + 'coat', + 'coauthor', + 'cobalt', + 'cobbler', + 'cobweb', + 'cocoa', + 'coconut', + 'cod', + 'coeditor', + 'coerce', + 'coexist', + 'coffee', + 'cofounder', + 'cognition', + 'cognitive', + 'cogwheel', + 'coherence', + 'coherent', + 'cohesive', + 'coil', + 'coke', + 'cola', + 'cold', + 'coleslaw', + 'coliseum', + 'collage', + 'collapse', + 'collar', + 'collected', + 'collector', + 'collide', + 'collie', + 'collision', + 'colonial', + 'colonist', + 'colonize', + 'colony', + 'colossal', + 'colt', + 'coma', + 'come', + 'comfort', + 'comfy', + 'comic', + 'coming', + 'comma', + 'commence', + 'commend', + 'comment', + 'commerce', + 'commode', + 'commodity', + 'commodore', + 'common', + 'commotion', + 'commute', + 'commuting', + 'compacted', + 'compacter', + 'compactly', + 'compactor', + 'companion', + 'company', + 'compare', + 'compel', + 'compile', + 'comply', + 'component', + 'composed', + 'composer', + 'composite', + 'compost', + 'composure', + 'compound', + 'compress', + 'comprised', + 'computer', + 'computing', + 'comrade', + 'concave', + 'conceal', + 'conceded', + 'concept', + 'concerned', + 'concert', + 'conch', + 'concierge', + 'concise', + 'conclude', + 'concrete', + 'concur', + 'condense', + 'condiment', + 'condition', + 'condone', + 'conducive', + 'conductor', + 'conduit', + 'cone', + 'confess', + 'confetti', + 'confidant', + 'confident', + 'confider', + 'confiding', + 'configure', + 'confined', + 'confining', + 'confirm', + 'conflict', + 'conform', + 'confound', + 'confront', + 'confused', + 'confusing', + 'confusion', + 'congenial', + 'congested', + 'congrats', + 'congress', + 'conical', + 'conjoined', + 'conjure', + 'conjuror', + 'connected', + 'connector', + 'consensus', + 'consent', + 'console', + 'consoling', + 'consonant', + 'constable', + 'constant', + 'constrain', + 'constrict', + 'construct', + 'consult', + 'consumer', + 'consuming', + 'contact', + 'container', + 'contempt', + 'contend', + 'contented', + 'contently', + 'contents', + 'contest', + 'context', + 'contort', + 'contour', + 'contrite', + 'control', + 'contusion', + 'convene', + 'convent', + 'copartner', + 'cope', + 'copied', + 'copier', + 'copilot', + 'coping', + 'copious', + 'copper', + 'copy', + 'coral', + 'cork', + 'cornball', + 'cornbread', + 'corncob', + 'cornea', + 'corned', + 'corner', + 'cornfield', + 'cornflake', + 'cornhusk', + 'cornmeal', + 'cornstalk', + 'corny', + 'coronary', + 'coroner', + 'corporal', + 'corporate', + 'corral', + 'correct', + 'corridor', + 'corrode', + 'corroding', + 'corrosive', + 'corsage', + 'corset', + 'cortex', + 'cosigner', + 'cosmetics', + 'cosmic', + 'cosmos', + 'cosponsor', + 'cost', + 'cottage', + 'cotton', + 'couch', + 'cough', + 'could', + 'countable', + 'countdown', + 'counting', + 'countless', + 'country', + 'county', + 'courier', + 'covenant', + 'cover', + 'coveted', + 'coveting', + 'coyness', + 'cozily', + 'coziness', + 'cozy', + 'crabbing', + 'crabgrass', + 'crablike', + 'crabmeat', + 'cradle', + 'cradling', + 'crafter', + 'craftily', + 'craftsman', + 'craftwork', + 'crafty', + 'cramp', + 'cranberry', + 'crane', + 'cranial', + 'cranium', + 'crank', + 'crate', + 'crave', + 'craving', + 'crawfish', + 'crawlers', + 'crawling', + 'crayfish', + 'crayon', + 'crazed', + 'crazily', + 'craziness', + 'crazy', + 'creamed', + 'creamer', + 'creamlike', + 'crease', + 'creasing', + 'creatable', + 'create', + 'creation', + 'creative', + 'creature', + 'credible', + 'credibly', + 'credit', + 'creed', + 'creme', + 'creole', + 'crepe', + 'crept', + 'crescent', + 'crested', + 'cresting', + 'crestless', + 'crevice', + 'crewless', + 'crewman', + 'crewmate', + 'crib', + 'cricket', + 'cried', + 'crier', + 'crimp', + 'crimson', + 'cringe', + 'cringing', + 'crinkle', + 'crinkly', + 'crisped', + 'crisping', + 'crisply', + 'crispness', + 'crispy', + 'criteria', + 'critter', + 'croak', + 'crock', + 'crook', + 'croon', + 'crop', + 'cross', + 'crouch', + 'crouton', + 'crowbar', + 'crowd', + 'crown', + 'crucial', + 'crudely', + 'crudeness', + 'cruelly', + 'cruelness', + 'cruelty', + 'crumb', + 'crummiest', + 'crummy', + 'crumpet', + 'crumpled', + 'cruncher', + 'crunching', + 'crunchy', + 'crusader', + 'crushable', + 'crushed', + 'crusher', + 'crushing', + 'crust', + 'crux', + 'crying', + 'cryptic', + 'crystal', + 'cubbyhole', + 'cube', + 'cubical', + 'cubicle', + 'cucumber', + 'cuddle', + 'cuddly', + 'cufflink', + 'culinary', + 'culminate', + 'culpable', + 'culprit', + 'cultivate', + 'cultural', + 'culture', + 'cupbearer', + 'cupcake', + 'cupid', + 'cupped', + 'cupping', + 'curable', + 'curator', + 'curdle', + 'cure', + 'curfew', + 'curing', + 'curled', + 'curler', + 'curliness', + 'curling', + 'curly', + 'curry', + 'curse', + 'cursive', + 'cursor', + 'curtain', + 'curtly', + 'curtsy', + 'curvature', + 'curve', + 'curvy', + 'cushy', + 'cusp', + 'cussed', + 'custard', + 'custodian', + 'custody', + 'customary', + 'customer', + 'customize', + 'customs', + 'cut', + 'cycle', + 'cyclic', + 'cycling', + 'cyclist', + 'cylinder', + 'cymbal', + 'cytoplasm', + 'cytoplast', + 'dab', + 'dad', + 'daffodil', + 'dagger', + 'daily', + 'daintily', + 'dainty', + 'dairy', + 'daisy', + 'dallying', + 'dance', + 'dancing', + 'dandelion', + 'dander', + 'dandruff', + 'dandy', + 'danger', + 'dangle', + 'dangling', + 'daredevil', + 'dares', + 'daringly', + 'darkened', + 'darkening', + 'darkish', + 'darkness', + 'darkroom', + 'darling', + 'darn', + 'dart', + 'darwinism', + 'dash', + 'dastardly', + 'data', + 'datebook', + 'dating', + 'daughter', + 'daunting', + 'dawdler', + 'dawn', + 'daybed', + 'daybreak', + 'daycare', + 'daydream', + 'daylight', + 'daylong', + 'dayroom', + 'daytime', + 'dazzler', + 'dazzling', + 'deacon', + 'deafening', + 'deafness', + 'dealer', + 'dealing', + 'dealmaker', + 'dealt', + 'dean', + 'debatable', + 'debate', + 'debating', + 'debit', + 'debrief', + 'debtless', + 'debtor', + 'debug', + 'debunk', + 'decade', + 'decaf', + 'decal', + 'decathlon', + 'decay', + 'deceased', + 'deceit', + 'deceiver', + 'deceiving', + 'december', + 'decency', + 'decent', + 'deception', + 'deceptive', + 'decibel', + 'decidable', + 'decimal', + 'decimeter', + 'decipher', + 'deck', + 'declared', + 'decline', + 'decode', + 'decompose', + 'decorated', + 'decorator', + 'decoy', + 'decrease', + 'decree', + 'dedicate', + 'dedicator', + 'deduce', + 'deduct', + 'deed', + 'deem', + 'deepen', + 'deeply', + 'deepness', + 'deface', + 'defacing', + 'defame', + 'default', + 'defeat', + 'defection', + 'defective', + 'defendant', + 'defender', + 'defense', + 'defensive', + 'deferral', + 'deferred', + 'defiance', + 'defiant', + 'defile', + 'defiling', + 'define', + 'definite', + 'deflate', + 'deflation', + 'deflator', + 'deflected', + 'deflector', + 'defog', + 'deforest', + 'defraud', + 'defrost', + 'deftly', + 'defuse', + 'defy', + 'degraded', + 'degrading', + 'degrease', + 'degree', + 'dehydrate', + 'deity', + 'dejected', + 'delay', + 'delegate', + 'delegator', + 'delete', + 'deletion', + 'delicacy', + 'delicate', + 'delicious', + 'delighted', + 'delirious', + 'delirium', + 'deliverer', + 'delivery', + 'delouse', + 'delta', + 'deluge', + 'delusion', + 'deluxe', + 'demanding', + 'demeaning', + 'demeanor', + 'demise', + 'democracy', + 'democrat', + 'demote', + 'demotion', + 'demystify', + 'denatured', + 'deniable', + 'denial', + 'denim', + 'denote', + 'dense', + 'density', + 'dental', + 'dentist', + 'denture', + 'deny', + 'deodorant', + 'deodorize', + 'departed', + 'departure', + 'depict', + 'deplete', + 'depletion', + 'deplored', + 'deploy', + 'deport', + 'depose', + 'depraved', + 'depravity', + 'deprecate', + 'depress', + 'deprive', + 'depth', + 'deputize', + 'deputy', + 'derail', + 'deranged', + 'derby', + 'derived', + 'desecrate', + 'deserve', + 'deserving', + 'designate', + 'designed', + 'designer', + 'designing', + 'deskbound', + 'desktop', + 'deskwork', + 'desolate', + 'despair', + 'despise', + 'despite', + 'destiny', + 'destitute', + 'destruct', + 'detached', + 'detail', + 'detection', + 'detective', + 'detector', + 'detention', + 'detergent', + 'detest', + 'detonate', + 'detonator', + 'detoxify', + 'detract', + 'deuce', + 'devalue', + 'deviancy', + 'deviant', + 'deviate', + 'deviation', + 'deviator', + 'device', + 'devious', + 'devotedly', + 'devotee', + 'devotion', + 'devourer', + 'devouring', + 'devoutly', + 'dexterity', + 'dexterous', + 'diabetes', + 'diabetic', + 'diabolic', + 'diagnoses', + 'diagnosis', + 'diagram', + 'dial', + 'diameter', + 'diaper', + 'diaphragm', + 'diary', + 'dice', + 'dicing', + 'dictate', + 'dictation', + 'dictator', + 'difficult', + 'diffused', + 'diffuser', + 'diffusion', + 'diffusive', + 'dig', + 'dilation', + 'diligence', + 'diligent', + 'dill', + 'dilute', + 'dime', + 'diminish', + 'dimly', + 'dimmed', + 'dimmer', + 'dimness', + 'dimple', + 'diner', + 'dingbat', + 'dinghy', + 'dinginess', + 'dingo', + 'dingy', + 'dining', + 'dinner', + 'diocese', + 'dioxide', + 'diploma', + 'dipped', + 'dipper', + 'dipping', + 'directed', + 'direction', + 'directive', + 'directly', + 'directory', + 'direness', + 'dirtiness', + 'disabled', + 'disagree', + 'disallow', + 'disarm', + 'disarray', + 'disaster', + 'disband', + 'disbelief', + 'disburse', + 'discard', + 'discern', + 'discharge', + 'disclose', + 'discolor', + 'discount', + 'discourse', + 'discover', + 'discuss', + 'disdain', + 'disengage', + 'disfigure', + 'disgrace', + 'dish', + 'disinfect', + 'disjoin', + 'disk', + 'dislike', + 'disliking', + 'dislocate', + 'dislodge', + 'disloyal', + 'dismantle', + 'dismay', + 'dismiss', + 'dismount', + 'disobey', + 'disorder', + 'disown', + 'disparate', + 'disparity', + 'dispatch', + 'dispense', + 'dispersal', + 'dispersed', + 'disperser', + 'displace', + 'display', + 'displease', + 'disposal', + 'dispose', + 'disprove', + 'dispute', + 'disregard', + 'disrupt', + 'dissuade', + 'distance', + 'distant', + 'distaste', + 'distill', + 'distinct', + 'distort', + 'distract', + 'distress', + 'district', + 'distrust', + 'ditch', + 'ditto', + 'ditzy', + 'dividable', + 'divided', + 'dividend', + 'dividers', + 'dividing', + 'divinely', + 'diving', + 'divinity', + 'divisible', + 'divisibly', + 'division', + 'divisive', + 'divorcee', + 'dizziness', + 'dizzy', + 'doable', + 'docile', + 'dock', + 'doctrine', + 'document', + 'dodge', + 'dodgy', + 'doily', + 'doing', + 'dole', + 'dollar', + 'dollhouse', + 'dollop', + 'dolly', + 'dolphin', + 'domain', + 'domelike', + 'domestic', + 'dominion', + 'dominoes', + 'donated', + 'donation', + 'donator', + 'donor', + 'donut', + 'doodle', + 'doorbell', + 'doorframe', + 'doorknob', + 'doorman', + 'doormat', + 'doornail', + 'doorpost', + 'doorstep', + 'doorstop', + 'doorway', + 'doozy', + 'dork', + 'dormitory', + 'dorsal', + 'dosage', + 'dose', + 'dotted', + 'doubling', + 'douche', + 'dove', + 'down', + 'dowry', + 'doze', + 'drab', + 'dragging', + 'dragonfly', + 'dragonish', + 'dragster', + 'drainable', + 'drainage', + 'drained', + 'drainer', + 'drainpipe', + 'dramatic', + 'dramatize', + 'drank', + 'drapery', + 'drastic', + 'draw', + 'dreaded', + 'dreadful', + 'dreadlock', + 'dreamboat', + 'dreamily', + 'dreamland', + 'dreamless', + 'dreamlike', + 'dreamt', + 'dreamy', + 'drearily', + 'dreary', + 'drench', + 'dress', + 'drew', + 'dribble', + 'dried', + 'drier', + 'drift', + 'driller', + 'drilling', + 'drinkable', + 'drinking', + 'dripping', + 'drippy', + 'drivable', + 'driven', + 'driver', + 'driveway', + 'driving', + 'drizzle', + 'drizzly', + 'drone', + 'drool', + 'droop', + 'drop-down', + 'dropbox', + 'dropkick', + 'droplet', + 'dropout', + 'dropper', + 'drove', + 'drown', + 'drowsily', + 'drudge', + 'drum', + 'dry', + 'dubbed', + 'dubiously', + 'duchess', + 'duckbill', + 'ducking', + 'duckling', + 'ducktail', + 'ducky', + 'duct', + 'dude', + 'duffel', + 'dugout', + 'duh', + 'duke', + 'duller', + 'dullness', + 'duly', + 'dumping', + 'dumpling', + 'dumpster', + 'duo', + 'dupe', + 'duplex', + 'duplicate', + 'duplicity', + 'durable', + 'durably', + 'duration', + 'duress', + 'during', + 'dusk', + 'dust', + 'dutiful', + 'duty', + 'duvet', + 'dwarf', + 'dweeb', + 'dwelled', + 'dweller', + 'dwelling', + 'dwindle', + 'dwindling', + 'dynamic', + 'dynamite', + 'dynasty', + 'dyslexia', + 'dyslexic', + 'each', + 'eagle', + 'earache', + 'eardrum', + 'earflap', + 'earful', + 'earlobe', + 'early', + 'earmark', + 'earmuff', + 'earphone', + 'earpiece', + 'earplugs', + 'earring', + 'earshot', + 'earthen', + 'earthlike', + 'earthling', + 'earthly', + 'earthworm', + 'earthy', + 'earwig', + 'easeful', + 'easel', + 'easiest', + 'easily', + 'easiness', + 'easing', + 'eastbound', + 'eastcoast', + 'easter', + 'eastward', + 'eatable', + 'eaten', + 'eatery', + 'eating', + 'eats', + 'ebay', + 'ebony', + 'ebook', + 'ecard', + 'eccentric', + 'echo', + 'eclair', + 'eclipse', + 'ecologist', + 'ecology', + 'economic', + 'economist', + 'economy', + 'ecosphere', + 'ecosystem', + 'edge', + 'edginess', + 'edging', + 'edgy', + 'edition', + 'editor', + 'educated', + 'education', + 'educator', + 'eel', + 'effective', + 'effects', + 'efficient', + 'effort', + 'eggbeater', + 'egging', + 'eggnog', + 'eggplant', + 'eggshell', + 'egomaniac', + 'egotism', + 'egotistic', + 'either', + 'eject', + 'elaborate', + 'elastic', + 'elated', + 'elbow', + 'eldercare', + 'elderly', + 'eldest', + 'electable', + 'election', + 'elective', + 'elephant', + 'elevate', + 'elevating', + 'elevation', + 'elevator', + 'eleven', + 'elf', + 'eligible', + 'eligibly', + 'eliminate', + 'elite', + 'elitism', + 'elixir', + 'elk', + 'ellipse', + 'elliptic', + 'elm', + 'elongated', + 'elope', + 'eloquence', + 'eloquent', + 'elsewhere', + 'elude', + 'elusive', + 'elves', + 'email', + 'embargo', + 'embark', + 'embassy', + 'embattled', + 'embellish', + 'ember', + 'embezzle', + 'emblaze', + 'emblem', + 'embody', + 'embolism', + 'emboss', + 'embroider', + 'emcee', + 'emerald', + 'emergency', + 'emission', + 'emit', + 'emote', + 'emoticon', + 'emotion', + 'empathic', + 'empathy', + 'emperor', + 'emphases', + 'emphasis', + 'emphasize', + 'emphatic', + 'empirical', + 'employed', + 'employee', + 'employer', + 'emporium', + 'empower', + 'emptier', + 'emptiness', + 'empty', + 'emu', + 'enable', + 'enactment', + 'enamel', + 'enchanted', + 'enchilada', + 'encircle', + 'enclose', + 'enclosure', + 'encode', + 'encore', + 'encounter', + 'encourage', + 'encroach', + 'encrust', + 'encrypt', + 'endanger', + 'endeared', + 'endearing', + 'ended', + 'ending', + 'endless', + 'endnote', + 'endocrine', + 'endorphin', + 'endorse', + 'endowment', + 'endpoint', + 'endurable', + 'endurance', + 'enduring', + 'energetic', + 'energize', + 'energy', + 'enforced', + 'enforcer', + 'engaged', + 'engaging', + 'engine', + 'engorge', + 'engraved', + 'engraver', + 'engraving', + 'engross', + 'engulf', + 'enhance', + 'enigmatic', + 'enjoyable', + 'enjoyably', + 'enjoyer', + 'enjoying', + 'enjoyment', + 'enlarged', + 'enlarging', + 'enlighten', + 'enlisted', + 'enquirer', + 'enrage', + 'enrich', + 'enroll', + 'enslave', + 'ensnare', + 'ensure', + 'entail', + 'entangled', + 'entering', + 'entertain', + 'enticing', + 'entire', + 'entitle', + 'entity', + 'entomb', + 'entourage', + 'entrap', + 'entree', + 'entrench', + 'entrust', + 'entryway', + 'entwine', + 'enunciate', + 'envelope', + 'enviable', + 'enviably', + 'envious', + 'envision', + 'envoy', + 'envy', + 'enzyme', + 'epic', + 'epidemic', + 'epidermal', + 'epidermis', + 'epidural', + 'epilepsy', + 'epileptic', + 'epilogue', + 'epiphany', + 'episode', + 'equal', + 'equate', + 'equation', + 'equator', + 'equinox', + 'equipment', + 'equity', + 'equivocal', + 'eradicate', + 'erasable', + 'erased', + 'eraser', + 'erasure', + 'ergonomic', + 'errand', + 'errant', + 'erratic', + 'error', + 'erupt', + 'escalate', + 'escalator', + 'escapable', + 'escapade', + 'escapist', + 'escargot', + 'eskimo', + 'esophagus', + 'espionage', + 'espresso', + 'esquire', + 'essay', + 'essence', + 'essential', + 'establish', + 'estate', + 'esteemed', + 'estimate', + 'estimator', + 'estranged', + 'estrogen', + 'etching', + 'eternal', + 'eternity', + 'ethanol', + 'ether', + 'ethically', + 'ethics', + 'euphemism', + 'evacuate', + 'evacuee', + 'evade', + 'evaluate', + 'evaluator', + 'evaporate', + 'evasion', + 'evasive', + 'even', + 'everglade', + 'evergreen', + 'everybody', + 'everyday', + 'everyone', + 'evict', + 'evidence', + 'evident', + 'evil', + 'evoke', + 'evolution', + 'evolve', + 'exact', + 'exalted', + 'example', + 'excavate', + 'excavator', + 'exceeding', + 'exception', + 'excess', + 'exchange', + 'excitable', + 'exciting', + 'exclaim', + 'exclude', + 'excluding', + 'exclusion', + 'exclusive', + 'excretion', + 'excretory', + 'excursion', + 'excusable', + 'excusably', + 'excuse', + 'exemplary', + 'exemplify', + 'exemption', + 'exerciser', + 'exert', + 'exes', + 'exfoliate', + 'exhale', + 'exhaust', + 'exhume', + 'exile', + 'existing', + 'exit', + 'exodus', + 'exonerate', + 'exorcism', + 'exorcist', + 'expand', + 'expanse', + 'expansion', + 'expansive', + 'expectant', + 'expedited', + 'expediter', + 'expel', + 'expend', + 'expenses', + 'expensive', + 'expert', + 'expire', + 'expiring', + 'explain', + 'expletive', + 'explicit', + 'explode', + 'exploit', + 'explore', + 'exploring', + 'exponent', + 'exporter', + 'exposable', + 'expose', + 'exposure', + 'express', + 'expulsion', + 'exquisite', + 'extended', + 'extending', + 'extent', + 'extenuate', + 'exterior', + 'external', + 'extinct', + 'extortion', + 'extradite', + 'extras', + 'extrovert', + 'extrude', + 'extruding', + 'exuberant', + 'fable', + 'fabric', + 'fabulous', + 'facebook', + 'facecloth', + 'facedown', + 'faceless', + 'facelift', + 'faceplate', + 'faceted', + 'facial', + 'facility', + 'facing', + 'facsimile', + 'faction', + 'factoid', + 'factor', + 'factsheet', + 'factual', + 'faculty', + 'fade', + 'fading', + 'failing', + 'falcon', + 'fall', + 'false', + 'falsify', + 'fame', + 'familiar', + 'family', + 'famine', + 'famished', + 'fanatic', + 'fancied', + 'fanciness', + 'fancy', + 'fanfare', + 'fang', + 'fanning', + 'fantasize', + 'fantastic', + 'fantasy', + 'fascism', + 'fastball', + 'faster', + 'fasting', + 'fastness', + 'faucet', + 'favorable', + 'favorably', + 'favored', + 'favoring', + 'favorite', + 'fax', + 'feast', + 'federal', + 'fedora', + 'feeble', + 'feed', + 'feel', + 'feisty', + 'feline', + 'felt-tip', + 'feminine', + 'feminism', + 'feminist', + 'feminize', + 'femur', + 'fence', + 'fencing', + 'fender', + 'ferment', + 'fernlike', + 'ferocious', + 'ferocity', + 'ferret', + 'ferris', + 'ferry', + 'fervor', + 'fester', + 'festival', + 'festive', + 'festivity', + 'fetal', + 'fetch', + 'fever', + 'fiber', + 'fiction', + 'fiddle', + 'fiddling', + 'fidelity', + 'fidgeting', + 'fidgety', + 'fifteen', + 'fifth', + 'fiftieth', + 'fifty', + 'figment', + 'figure', + 'figurine', + 'filing', + 'filled', + 'filler', + 'filling', + 'film', + 'filter', + 'filth', + 'filtrate', + 'finale', + 'finalist', + 'finalize', + 'finally', + 'finance', + 'financial', + 'finch', + 'fineness', + 'finer', + 'finicky', + 'finished', + 'finisher', + 'finishing', + 'finite', + 'finless', + 'finlike', + 'fiscally', + 'fit', + 'five', + 'flaccid', + 'flagman', + 'flagpole', + 'flagship', + 'flagstick', + 'flagstone', + 'flail', + 'flakily', + 'flaky', + 'flame', + 'flammable', + 'flanked', + 'flanking', + 'flannels', + 'flap', + 'flaring', + 'flashback', + 'flashbulb', + 'flashcard', + 'flashily', + 'flashing', + 'flashy', + 'flask', + 'flatbed', + 'flatfoot', + 'flatly', + 'flatness', + 'flatten', + 'flattered', + 'flatterer', + 'flattery', + 'flattop', + 'flatware', + 'flatworm', + 'flavored', + 'flavorful', + 'flavoring', + 'flaxseed', + 'fled', + 'fleshed', + 'fleshy', + 'flick', + 'flier', + 'flight', + 'flinch', + 'fling', + 'flint', + 'flip', + 'flirt', + 'float', + 'flock', + 'flogging', + 'flop', + 'floral', + 'florist', + 'floss', + 'flounder', + 'flyable', + 'flyaway', + 'flyer', + 'flying', + 'flyover', + 'flypaper', + 'foam', + 'foe', + 'fog', + 'foil', + 'folic', + 'folk', + 'follicle', + 'follow', + 'fondling', + 'fondly', + 'fondness', + 'fondue', + 'font', + 'food', + 'fool', + 'footage', + 'football', + 'footbath', + 'footboard', + 'footer', + 'footgear', + 'foothill', + 'foothold', + 'footing', + 'footless', + 'footman', + 'footnote', + 'footpad', + 'footpath', + 'footprint', + 'footrest', + 'footsie', + 'footsore', + 'footwear', + 'footwork', + 'fossil', + 'foster', + 'founder', + 'founding', + 'fountain', + 'fox', + 'foyer', + 'fraction', + 'fracture', + 'fragile', + 'fragility', + 'fragment', + 'fragrance', + 'fragrant', + 'frail', + 'frame', + 'framing', + 'frantic', + 'fraternal', + 'frayed', + 'fraying', + 'frays', + 'freckled', + 'freckles', + 'freebase', + 'freebee', + 'freebie', + 'freedom', + 'freefall', + 'freehand', + 'freeing', + 'freeload', + 'freely', + 'freemason', + 'freeness', + 'freestyle', + 'freeware', + 'freeway', + 'freewill', + 'freezable', + 'freezing', + 'freight', + 'french', + 'frenzied', + 'frenzy', + 'frequency', + 'frequent', + 'fresh', + 'fretful', + 'fretted', + 'friction', + 'friday', + 'fridge', + 'fried', + 'friend', + 'frighten', + 'frightful', + 'frigidity', + 'frigidly', + 'frill', + 'fringe', + 'frisbee', + 'frisk', + 'fritter', + 'frivolous', + 'frolic', + 'from', + 'front', + 'frostbite', + 'frosted', + 'frostily', + 'frosting', + 'frostlike', + 'frosty', + 'froth', + 'frown', + 'frozen', + 'fructose', + 'frugality', + 'frugally', + 'fruit', + 'frustrate', + 'frying', + 'gab', + 'gaffe', + 'gag', + 'gainfully', + 'gaining', + 'gains', + 'gala', + 'gallantly', + 'galleria', + 'gallery', + 'galley', + 'gallon', + 'gallows', + 'gallstone', + 'galore', + 'galvanize', + 'gambling', + 'game', + 'gaming', + 'gamma', + 'gander', + 'gangly', + 'gangrene', + 'gangway', + 'gap', + 'garage', + 'garbage', + 'garden', + 'gargle', + 'garland', + 'garlic', + 'garment', + 'garnet', + 'garnish', + 'garter', + 'gas', + 'gatherer', + 'gathering', + 'gating', + 'gauging', + 'gauntlet', + 'gauze', + 'gave', + 'gawk', + 'gazing', + 'gear', + 'gecko', + 'geek', + 'geiger', + 'gem', + 'gender', + 'generic', + 'generous', + 'genetics', + 'genre', + 'gentile', + 'gentleman', + 'gently', + 'gents', + 'geography', + 'geologic', + 'geologist', + 'geology', + 'geometric', + 'geometry', + 'geranium', + 'gerbil', + 'geriatric', + 'germicide', + 'germinate', + 'germless', + 'germproof', + 'gestate', + 'gestation', + 'gesture', + 'getaway', + 'getting', + 'getup', + 'giant', + 'gibberish', + 'giblet', + 'giddily', + 'giddiness', + 'giddy', + 'gift', + 'gigabyte', + 'gigahertz', + 'gigantic', + 'giggle', + 'giggling', + 'giggly', + 'gigolo', + 'gilled', + 'gills', + 'gimmick', + 'girdle', + 'giveaway', + 'given', + 'giver', + 'giving', + 'gizmo', + 'gizzard', + 'glacial', + 'glacier', + 'glade', + 'gladiator', + 'gladly', + 'glamorous', + 'glamour', + 'glance', + 'glancing', + 'glandular', + 'glare', + 'glaring', + 'glass', + 'glaucoma', + 'glazing', + 'gleaming', + 'gleeful', + 'glider', + 'gliding', + 'glimmer', + 'glimpse', + 'glisten', + 'glitch', + 'glitter', + 'glitzy', + 'gloater', + 'gloating', + 'gloomily', + 'gloomy', + 'glorified', + 'glorifier', + 'glorify', + 'glorious', + 'glory', + 'gloss', + 'glove', + 'glowing', + 'glowworm', + 'glucose', + 'glue', + 'gluten', + 'glutinous', + 'glutton', + 'gnarly', + 'gnat', + 'goal', + 'goatskin', + 'goes', + 'goggles', + 'going', + 'goldfish', + 'goldmine', + 'goldsmith', + 'golf', + 'goliath', + 'gonad', + 'gondola', + 'gone', + 'gong', + 'good', + 'gooey', + 'goofball', + 'goofiness', + 'goofy', + 'google', + 'goon', + 'gopher', + 'gore', + 'gorged', + 'gorgeous', + 'gory', + 'gosling', + 'gossip', + 'gothic', + 'gotten', + 'gout', + 'gown', + 'grab', + 'graceful', + 'graceless', + 'gracious', + 'gradation', + 'graded', + 'grader', + 'gradient', + 'grading', + 'gradually', + 'graduate', + 'graffiti', + 'grafted', + 'grafting', + 'grain', + 'granddad', + 'grandkid', + 'grandly', + 'grandma', + 'grandpa', + 'grandson', + 'granite', + 'granny', + 'granola', + 'grant', + 'granular', + 'grape', + 'graph', + 'grapple', + 'grappling', + 'grasp', + 'grass', + 'gratified', + 'gratify', + 'grating', + 'gratitude', + 'gratuity', + 'gravel', + 'graveness', + 'graves', + 'graveyard', + 'gravitate', + 'gravity', + 'gravy', + 'gray', + 'grazing', + 'greasily', + 'greedily', + 'greedless', + 'greedy', + 'green', + 'greeter', + 'greeting', + 'grew', + 'greyhound', + 'grid', + 'grief', + 'grievance', + 'grieving', + 'grievous', + 'grill', + 'grimace', + 'grimacing', + 'grime', + 'griminess', + 'grimy', + 'grinch', + 'grinning', + 'grip', + 'gristle', + 'grit', + 'groggily', + 'groggy', + 'groin', + 'groom', + 'groove', + 'grooving', + 'groovy', + 'grope', + 'ground', + 'grouped', + 'grout', + 'grove', + 'grower', + 'growing', + 'growl', + 'grub', + 'grudge', + 'grudging', + 'grueling', + 'gruffly', + 'grumble', + 'grumbling', + 'grumbly', + 'grumpily', + 'grunge', + 'grunt', + 'guacamole', + 'guidable', + 'guidance', + 'guide', + 'guiding', + 'guileless', + 'guise', + 'gulf', + 'gullible', + 'gully', + 'gulp', + 'gumball', + 'gumdrop', + 'gumminess', + 'gumming', + 'gummy', + 'gurgle', + 'gurgling', + 'guru', + 'gush', + 'gusto', + 'gusty', + 'gutless', + 'guts', + 'gutter', + 'guy', + 'guzzler', + 'gyration', + 'habitable', + 'habitant', + 'habitat', + 'habitual', + 'hacked', + 'hacker', + 'hacking', + 'hacksaw', + 'had', + 'haggler', + 'haiku', + 'half', + 'halogen', + 'halt', + 'halved', + 'halves', + 'hamburger', + 'hamlet', + 'hammock', + 'hamper', + 'hamster', + 'hamstring', + 'handbag', + 'handball', + 'handbook', + 'handbrake', + 'handcart', + 'handclap', + 'handclasp', + 'handcraft', + 'handcuff', + 'handed', + 'handful', + 'handgrip', + 'handgun', + 'handheld', + 'handiness', + 'handiwork', + 'handlebar', + 'handled', + 'handler', + 'handling', + 'handmade', + 'handoff', + 'handpick', + 'handprint', + 'handrail', + 'handsaw', + 'handset', + 'handsfree', + 'handshake', + 'handstand', + 'handwash', + 'handwork', + 'handwoven', + 'handwrite', + 'handyman', + 'hangnail', + 'hangout', + 'hangover', + 'hangup', + 'hankering', + 'hankie', + 'hanky', + 'haphazard', + 'happening', + 'happier', + 'happiest', + 'happily', + 'happiness', + 'happy', + 'harbor', + 'hardcopy', + 'hardcore', + 'hardcover', + 'harddisk', + 'hardened', + 'hardener', + 'hardening', + 'hardhat', + 'hardhead', + 'hardiness', + 'hardly', + 'hardness', + 'hardship', + 'hardware', + 'hardwired', + 'hardwood', + 'hardy', + 'harmful', + 'harmless', + 'harmonica', + 'harmonics', + 'harmonize', + 'harmony', + 'harness', + 'harpist', + 'harsh', + 'harvest', + 'hash', + 'hassle', + 'haste', + 'hastily', + 'hastiness', + 'hasty', + 'hatbox', + 'hatchback', + 'hatchery', + 'hatchet', + 'hatching', + 'hatchling', + 'hate', + 'hatless', + 'hatred', + 'haunt', + 'haven', + 'hazard', + 'hazelnut', + 'hazily', + 'haziness', + 'hazing', + 'hazy', + 'headache', + 'headband', + 'headboard', + 'headcount', + 'headdress', + 'headed', + 'header', + 'headfirst', + 'headgear', + 'heading', + 'headlamp', + 'headless', + 'headlock', + 'headphone', + 'headpiece', + 'headrest', + 'headroom', + 'headscarf', + 'headset', + 'headsman', + 'headstand', + 'headstone', + 'headway', + 'headwear', + 'heap', + 'heat', + 'heave', + 'heavily', + 'heaviness', + 'heaving', + 'hedge', + 'hedging', + 'heftiness', + 'hefty', + 'helium', + 'helmet', + 'helper', + 'helpful', + 'helping', + 'helpless', + 'helpline', + 'hemlock', + 'hemstitch', + 'hence', + 'henchman', + 'henna', + 'herald', + 'herbal', + 'herbicide', + 'herbs', + 'heritage', + 'hermit', + 'heroics', + 'heroism', + 'herring', + 'herself', + 'hertz', + 'hesitancy', + 'hesitant', + 'hesitate', + 'hexagon', + 'hexagram', + 'hubcap', + 'huddle', + 'huddling', + 'huff', + 'hug', + 'hula', + 'hulk', + 'hull', + 'human', + 'humble', + 'humbling', + 'humbly', + 'humid', + 'humiliate', + 'humility', + 'humming', + 'hummus', + 'humongous', + 'humorist', + 'humorless', + 'humorous', + 'humpback', + 'humped', + 'humvee', + 'hunchback', + 'hundredth', + 'hunger', + 'hungrily', + 'hungry', + 'hunk', + 'hunter', + 'hunting', + 'huntress', + 'huntsman', + 'hurdle', + 'hurled', + 'hurler', + 'hurling', + 'hurray', + 'hurricane', + 'hurried', + 'hurry', + 'hurt', + 'husband', + 'hush', + 'husked', + 'huskiness', + 'hut', + 'hybrid', + 'hydrant', + 'hydrated', + 'hydration', + 'hydrogen', + 'hydroxide', + 'hyperlink', + 'hypertext', + 'hyphen', + 'hypnoses', + 'hypnosis', + 'hypnotic', + 'hypnotism', + 'hypnotist', + 'hypnotize', + 'hypocrisy', + 'hypocrite', + 'ibuprofen', + 'ice', + 'iciness', + 'icing', + 'icky', + 'icon', + 'icy', + 'idealism', + 'idealist', + 'idealize', + 'ideally', + 'idealness', + 'identical', + 'identify', + 'identity', + 'ideology', + 'idiocy', + 'idiom', + 'idly', + 'igloo', + 'ignition', + 'ignore', + 'iguana', + 'illicitly', + 'illusion', + 'illusive', + 'image', + 'imaginary', + 'imagines', + 'imaging', + 'imbecile', + 'imitate', + 'imitation', + 'immature', + 'immerse', + 'immersion', + 'imminent', + 'immobile', + 'immodest', + 'immorally', + 'immortal', + 'immovable', + 'immovably', + 'immunity', + 'immunize', + 'impaired', + 'impale', + 'impart', + 'impatient', + 'impeach', + 'impeding', + 'impending', + 'imperfect', + 'imperial', + 'impish', + 'implant', + 'implement', + 'implicate', + 'implicit', + 'implode', + 'implosion', + 'implosive', + 'imply', + 'impolite', + 'important', + 'importer', + 'impose', + 'imposing', + 'impotence', + 'impotency', + 'impotent', + 'impound', + 'imprecise', + 'imprint', + 'imprison', + 'impromptu', + 'improper', + 'improve', + 'improving', + 'improvise', + 'imprudent', + 'impulse', + 'impulsive', + 'impure', + 'impurity', + 'iodine', + 'iodize', + 'ion', + 'ipad', + 'iphone', + 'ipod', + 'irate', + 'irk', + 'iron', + 'irregular', + 'irrigate', + 'irritable', + 'irritably', + 'irritant', + 'irritate', + 'islamic', + 'islamist', + 'isolated', + 'isolating', + 'isolation', + 'isotope', + 'issue', + 'issuing', + 'italicize', + 'italics', + 'item', + 'itinerary', + 'itunes', + 'ivory', + 'ivy', + 'jab', + 'jackal', + 'jacket', + 'jackknife', + 'jackpot', + 'jailbird', + 'jailbreak', + 'jailer', + 'jailhouse', + 'jalapeno', + 'jam', + 'janitor', + 'january', + 'jargon', + 'jarring', + 'jasmine', + 'jaundice', + 'jaunt', + 'java', + 'jawed', + 'jawless', + 'jawline', + 'jaws', + 'jaybird', + 'jaywalker', + 'jazz', + 'jeep', + 'jeeringly', + 'jellied', + 'jelly', + 'jersey', + 'jester', + 'jet', + 'jiffy', + 'jigsaw', + 'jimmy', + 'jingle', + 'jingling', + 'jinx', + 'jitters', + 'jittery', + 'job', + 'jockey', + 'jockstrap', + 'jogger', + 'jogging', + 'john', + 'joining', + 'jokester', + 'jokingly', + 'jolliness', + 'jolly', + 'jolt', + 'jot', + 'jovial', + 'joyfully', + 'joylessly', + 'joyous', + 'joyride', + 'joystick', + 'jubilance', + 'jubilant', + 'judge', + 'judgingly', + 'judicial', + 'judiciary', + 'judo', + 'juggle', + 'juggling', + 'jugular', + 'juice', + 'juiciness', + 'juicy', + 'jujitsu', + 'jukebox', + 'july', + 'jumble', + 'jumbo', + 'jump', + 'junction', + 'juncture', + 'june', + 'junior', + 'juniper', + 'junkie', + 'junkman', + 'junkyard', + 'jurist', + 'juror', + 'jury', + 'justice', + 'justifier', + 'justify', + 'justly', + 'justness', + 'juvenile', + 'kabob', + 'kangaroo', + 'karaoke', + 'karate', + 'karma', + 'kebab', + 'keenly', + 'keenness', + 'keep', + 'keg', + 'kelp', + 'kennel', + 'kept', + 'kerchief', + 'kerosene', + 'kettle', + 'kick', + 'kiln', + 'kilobyte', + 'kilogram', + 'kilometer', + 'kilowatt', + 'kilt', + 'kimono', + 'kindle', + 'kindling', + 'kindly', + 'kindness', + 'kindred', + 'kinetic', + 'kinfolk', + 'king', + 'kinship', + 'kinsman', + 'kinswoman', + 'kissable', + 'kisser', + 'kissing', + 'kitchen', + 'kite', + 'kitten', + 'kitty', + 'kiwi', + 'kleenex', + 'knapsack', + 'knee', + 'knelt', + 'knickers', + 'knoll', + 'koala', + 'kooky', + 'kosher', + 'krypton', + 'kudos', + 'kung', + 'labored', + 'laborer', + 'laboring', + 'laborious', + 'labrador', + 'ladder', + 'ladies', + 'ladle', + 'ladybug', + 'ladylike', + 'lagged', + 'lagging', + 'lagoon', + 'lair', + 'lake', + 'lance', + 'landed', + 'landfall', + 'landfill', + 'landing', + 'landlady', + 'landless', + 'landline', + 'landlord', + 'landmark', + 'landmass', + 'landmine', + 'landowner', + 'landscape', + 'landside', + 'landslide', + 'language', + 'lankiness', + 'lanky', + 'lantern', + 'lapdog', + 'lapel', + 'lapped', + 'lapping', + 'laptop', + 'lard', + 'large', + 'lark', + 'lash', + 'lasso', + 'last', + 'latch', + 'late', + 'lather', + 'latitude', + 'latrine', + 'latter', + 'latticed', + 'launch', + 'launder', + 'laundry', + 'laurel', + 'lavender', + 'lavish', + 'laxative', + 'lazily', + 'laziness', + 'lazy', + 'lecturer', + 'left', + 'legacy', + 'legal', + 'legend', + 'legged', + 'leggings', + 'legible', + 'legibly', + 'legislate', + 'lego', + 'legroom', + 'legume', + 'legwarmer', + 'legwork', + 'lemon', + 'lend', + 'length', + 'lens', + 'lent', + 'leotard', + 'lesser', + 'letdown', + 'lethargic', + 'lethargy', + 'letter', + 'lettuce', + 'level', + 'leverage', + 'levers', + 'levitate', + 'levitator', + 'liability', + 'liable', + 'liberty', + 'librarian', + 'library', + 'licking', + 'licorice', + 'lid', + 'life', + 'lifter', + 'lifting', + 'liftoff', + 'ligament', + 'likely', + 'likeness', + 'likewise', + 'liking', + 'lilac', + 'lilly', + 'lily', + 'limb', + 'limeade', + 'limelight', + 'limes', + 'limit', + 'limping', + 'limpness', + 'line', + 'lingo', + 'linguini', + 'linguist', + 'lining', + 'linked', + 'linoleum', + 'linseed', + 'lint', + 'lion', + 'lip', + 'liquefy', + 'liqueur', + 'liquid', + 'lisp', + 'list', + 'litigate', + 'litigator', + 'litmus', + 'litter', + 'little', + 'livable', + 'lived', + 'lively', + 'liver', + 'livestock', + 'lividly', + 'living', + 'lizard', + 'lubricant', + 'lubricate', + 'lucid', + 'luckily', + 'luckiness', + 'luckless', + 'lucrative', + 'ludicrous', + 'lugged', + 'lukewarm', + 'lullaby', + 'lumber', + 'luminance', + 'luminous', + 'lumpiness', + 'lumping', + 'lumpish', + 'lunacy', + 'lunar', + 'lunchbox', + 'luncheon', + 'lunchroom', + 'lunchtime', + 'lung', + 'lurch', + 'lure', + 'luridness', + 'lurk', + 'lushly', + 'lushness', + 'luster', + 'lustfully', + 'lustily', + 'lustiness', + 'lustrous', + 'lusty', + 'luxurious', + 'luxury', + 'lying', + 'lyrically', + 'lyricism', + 'lyricist', + 'lyrics', + 'macarena', + 'macaroni', + 'macaw', + 'mace', + 'machine', + 'machinist', + 'magazine', + 'magenta', + 'maggot', + 'magical', + 'magician', + 'magma', + 'magnesium', + 'magnetic', + 'magnetism', + 'magnetize', + 'magnifier', + 'magnify', + 'magnitude', + 'magnolia', + 'mahogany', + 'maimed', + 'majestic', + 'majesty', + 'majorette', + 'majority', + 'makeover', + 'maker', + 'makeshift', + 'making', + 'malformed', + 'malt', + 'mama', + 'mammal', + 'mammary', + 'mammogram', + 'manager', + 'managing', + 'manatee', + 'mandarin', + 'mandate', + 'mandatory', + 'mandolin', + 'manger', + 'mangle', + 'mango', + 'mangy', + 'manhandle', + 'manhole', + 'manhood', + 'manhunt', + 'manicotti', + 'manicure', + 'manifesto', + 'manila', + 'mankind', + 'manlike', + 'manliness', + 'manly', + 'manmade', + 'manned', + 'mannish', + 'manor', + 'manpower', + 'mantis', + 'mantra', + 'manual', + 'many', + 'map', + 'marathon', + 'marauding', + 'marbled', + 'marbles', + 'marbling', + 'march', + 'mardi', + 'margarine', + 'margarita', + 'margin', + 'marigold', + 'marina', + 'marine', + 'marital', + 'maritime', + 'marlin', + 'marmalade', + 'maroon', + 'married', + 'marrow', + 'marry', + 'marshland', + 'marshy', + 'marsupial', + 'marvelous', + 'marxism', + 'mascot', + 'masculine', + 'mashed', + 'mashing', + 'massager', + 'masses', + 'massive', + 'mastiff', + 'matador', + 'matchbook', + 'matchbox', + 'matcher', + 'matching', + 'matchless', + 'material', + 'maternal', + 'maternity', + 'math', + 'mating', + 'matriarch', + 'matrimony', + 'matrix', + 'matron', + 'matted', + 'matter', + 'maturely', + 'maturing', + 'maturity', + 'mauve', + 'maverick', + 'maximize', + 'maximum', + 'maybe', + 'mayday', + 'mayflower', + 'moaner', + 'moaning', + 'mobile', + 'mobility', + 'mobilize', + 'mobster', + 'mocha', + 'mocker', + 'mockup', + 'modified', + 'modify', + 'modular', + 'modulator', + 'module', + 'moisten', + 'moistness', + 'moisture', + 'molar', + 'molasses', + 'mold', + 'molecular', + 'molecule', + 'molehill', + 'mollusk', + 'mom', + 'monastery', + 'monday', + 'monetary', + 'monetize', + 'moneybags', + 'moneyless', + 'moneywise', + 'mongoose', + 'mongrel', + 'monitor', + 'monkhood', + 'monogamy', + 'monogram', + 'monologue', + 'monopoly', + 'monorail', + 'monotone', + 'monotype', + 'monoxide', + 'monsieur', + 'monsoon', + 'monstrous', + 'monthly', + 'monument', + 'moocher', + 'moodiness', + 'moody', + 'mooing', + 'moonbeam', + 'mooned', + 'moonlight', + 'moonlike', + 'moonlit', + 'moonrise', + 'moonscape', + 'moonshine', + 'moonstone', + 'moonwalk', + 'mop', + 'morale', + 'morality', + 'morally', + 'morbidity', + 'morbidly', + 'morphine', + 'morphing', + 'morse', + 'mortality', + 'mortally', + 'mortician', + 'mortified', + 'mortify', + 'mortuary', + 'mosaic', + 'mossy', + 'most', + 'mothball', + 'mothproof', + 'motion', + 'motivate', + 'motivator', + 'motive', + 'motocross', + 'motor', + 'motto', + 'mountable', + 'mountain', + 'mounted', + 'mounting', + 'mourner', + 'mournful', + 'mouse', + 'mousiness', + 'moustache', + 'mousy', + 'mouth', + 'movable', + 'move', + 'movie', + 'moving', + 'mower', + 'mowing', + 'much', + 'muck', + 'mud', + 'mug', + 'mulberry', + 'mulch', + 'mule', + 'mulled', + 'mullets', + 'multiple', + 'multiply', + 'multitask', + 'multitude', + 'mumble', + 'mumbling', + 'mumbo', + 'mummified', + 'mummify', + 'mummy', + 'mumps', + 'munchkin', + 'mundane', + 'municipal', + 'muppet', + 'mural', + 'murkiness', + 'murky', + 'murmuring', + 'muscular', + 'museum', + 'mushily', + 'mushiness', + 'mushroom', + 'mushy', + 'music', + 'musket', + 'muskiness', + 'musky', + 'mustang', + 'mustard', + 'muster', + 'mustiness', + 'musty', + 'mutable', + 'mutate', + 'mutation', + 'mute', + 'mutilated', + 'mutilator', + 'mutiny', + 'mutt', + 'mutual', + 'muzzle', + 'myself', + 'myspace', + 'mystified', + 'mystify', + 'myth', + 'nacho', + 'nag', + 'nail', + 'name', + 'naming', + 'nanny', + 'nanometer', + 'nape', + 'napkin', + 'napped', + 'napping', + 'nappy', + 'narrow', + 'nastily', + 'nastiness', + 'national', + 'native', + 'nativity', + 'natural', + 'nature', + 'naturist', + 'nautical', + 'navigate', + 'navigator', + 'navy', + 'nearby', + 'nearest', + 'nearly', + 'nearness', + 'neatly', + 'neatness', + 'nebula', + 'nebulizer', + 'nectar', + 'negate', + 'negation', + 'negative', + 'neglector', + 'negligee', + 'negligent', + 'negotiate', + 'nemeses', + 'nemesis', + 'neon', + 'nephew', + 'nerd', + 'nervous', + 'nervy', + 'nest', + 'net', + 'neurology', + 'neuron', + 'neurosis', + 'neurotic', + 'neuter', + 'neutron', + 'never', + 'next', + 'nibble', + 'nickname', + 'nicotine', + 'niece', + 'nifty', + 'nimble', + 'nimbly', + 'nineteen', + 'ninetieth', + 'ninja', + 'nintendo', + 'ninth', + 'nuclear', + 'nuclei', + 'nucleus', + 'nugget', + 'nullify', + 'number', + 'numbing', + 'numbly', + 'numbness', + 'numeral', + 'numerate', + 'numerator', + 'numeric', + 'numerous', + 'nuptials', + 'nursery', + 'nursing', + 'nurture', + 'nutcase', + 'nutlike', + 'nutmeg', + 'nutrient', + 'nutshell', + 'nuttiness', + 'nutty', + 'nuzzle', + 'nylon', + 'oaf', + 'oak', + 'oasis', + 'oat', + 'obedience', + 'obedient', + 'obituary', + 'object', + 'obligate', + 'obliged', + 'oblivion', + 'oblivious', + 'oblong', + 'obnoxious', + 'oboe', + 'obscure', + 'obscurity', + 'observant', + 'observer', + 'observing', + 'obsessed', + 'obsession', + 'obsessive', + 'obsolete', + 'obstacle', + 'obstinate', + 'obstruct', + 'obtain', + 'obtrusive', + 'obtuse', + 'obvious', + 'occultist', + 'occupancy', + 'occupant', + 'occupier', + 'occupy', + 'ocean', + 'ocelot', + 'octagon', + 'octane', + 'october', + 'octopus', + 'ogle', + 'oil', + 'oink', + 'ointment', + 'okay', + 'old', + 'olive', + 'olympics', + 'omega', + 'omen', + 'ominous', + 'omission', + 'omit', + 'omnivore', + 'onboard', + 'oncoming', + 'ongoing', + 'onion', + 'online', + 'onlooker', + 'only', + 'onscreen', + 'onset', + 'onshore', + 'onslaught', + 'onstage', + 'onto', + 'onward', + 'onyx', + 'oops', + 'ooze', + 'oozy', + 'opacity', + 'opal', + 'open', + 'operable', + 'operate', + 'operating', + 'operation', + 'operative', + 'operator', + 'opium', + 'opossum', + 'opponent', + 'oppose', + 'opposing', + 'opposite', + 'oppressed', + 'oppressor', + 'opt', + 'opulently', + 'osmosis', + 'other', + 'otter', + 'ouch', + 'ought', + 'ounce', + 'outage', + 'outback', + 'outbid', + 'outboard', + 'outbound', + 'outbreak', + 'outburst', + 'outcast', + 'outclass', + 'outcome', + 'outdated', + 'outdoors', + 'outer', + 'outfield', + 'outfit', + 'outflank', + 'outgoing', + 'outgrow', + 'outhouse', + 'outing', + 'outlast', + 'outlet', + 'outline', + 'outlook', + 'outlying', + 'outmatch', + 'outmost', + 'outnumber', + 'outplayed', + 'outpost', + 'outpour', + 'output', + 'outrage', + 'outrank', + 'outreach', + 'outright', + 'outscore', + 'outsell', + 'outshine', + 'outshoot', + 'outsider', + 'outskirts', + 'outsmart', + 'outsource', + 'outspoken', + 'outtakes', + 'outthink', + 'outward', + 'outweigh', + 'outwit', + 'oval', + 'ovary', + 'oven', + 'overact', + 'overall', + 'overarch', + 'overbid', + 'overbill', + 'overbite', + 'overblown', + 'overboard', + 'overbook', + 'overbuilt', + 'overcast', + 'overcoat', + 'overcome', + 'overcook', + 'overcrowd', + 'overdraft', + 'overdrawn', + 'overdress', + 'overdrive', + 'overdue', + 'overeager', + 'overeater', + 'overexert', + 'overfed', + 'overfeed', + 'overfill', + 'overflow', + 'overfull', + 'overgrown', + 'overhand', + 'overhang', + 'overhaul', + 'overhead', + 'overhear', + 'overheat', + 'overhung', + 'overjoyed', + 'overkill', + 'overlabor', + 'overlaid', + 'overlap', + 'overlay', + 'overload', + 'overlook', + 'overlord', + 'overlying', + 'overnight', + 'overpass', + 'overpay', + 'overplant', + 'overplay', + 'overpower', + 'overprice', + 'overrate', + 'overreach', + 'overreact', + 'override', + 'overripe', + 'overrule', + 'overrun', + 'overshoot', + 'overshot', + 'oversight', + 'oversized', + 'oversleep', + 'oversold', + 'overspend', + 'overstate', + 'overstay', + 'overstep', + 'overstock', + 'overstuff', + 'oversweet', + 'overtake', + 'overthrow', + 'overtime', + 'overtly', + 'overtone', + 'overture', + 'overturn', + 'overuse', + 'overvalue', + 'overview', + 'overwrite', + 'owl', + 'oxford', + 'oxidant', + 'oxidation', + 'oxidize', + 'oxidizing', + 'oxygen', + 'oxymoron', + 'oyster', + 'ozone', + 'paced', + 'pacemaker', + 'pacific', + 'pacifier', + 'pacifism', + 'pacifist', + 'pacify', + 'padded', + 'padding', + 'paddle', + 'paddling', + 'padlock', + 'pagan', + 'pager', + 'paging', + 'pajamas', + 'palace', + 'palatable', + 'palm', + 'palpable', + 'palpitate', + 'paltry', + 'pampered', + 'pamperer', + 'pampers', + 'pamphlet', + 'panama', + 'pancake', + 'pancreas', + 'panda', + 'pandemic', + 'pang', + 'panhandle', + 'panic', + 'panning', + 'panorama', + 'panoramic', + 'panther', + 'pantomime', + 'pantry', + 'pants', + 'pantyhose', + 'paparazzi', + 'papaya', + 'paper', + 'paprika', + 'papyrus', + 'parabola', + 'parachute', + 'parade', + 'paradox', + 'paragraph', + 'parakeet', + 'paralegal', + 'paralyses', + 'paralysis', + 'paralyze', + 'paramedic', + 'parameter', + 'paramount', + 'parasail', + 'parasite', + 'parasitic', + 'parcel', + 'parched', + 'parchment', + 'pardon', + 'parish', + 'parka', + 'parking', + 'parkway', + 'parlor', + 'parmesan', + 'parole', + 'parrot', + 'parsley', + 'parsnip', + 'partake', + 'parted', + 'parting', + 'partition', + 'partly', + 'partner', + 'partridge', + 'party', + 'passable', + 'passably', + 'passage', + 'passcode', + 'passenger', + 'passerby', + 'passing', + 'passion', + 'passive', + 'passivism', + 'passover', + 'passport', + 'password', + 'pasta', + 'pasted', + 'pastel', + 'pastime', + 'pastor', + 'pastrami', + 'pasture', + 'pasty', + 'patchwork', + 'patchy', + 'paternal', + 'paternity', + 'path', + 'patience', + 'patient', + 'patio', + 'patriarch', + 'patriot', + 'patrol', + 'patronage', + 'patronize', + 'pauper', + 'pavement', + 'paver', + 'pavestone', + 'pavilion', + 'paving', + 'pawing', + 'payable', + 'payback', + 'paycheck', + 'payday', + 'payee', + 'payer', + 'paying', + 'payment', + 'payphone', + 'payroll', + 'pebble', + 'pebbly', + 'pecan', + 'pectin', + 'peculiar', + 'peddling', + 'pediatric', + 'pedicure', + 'pedigree', + 'pedometer', + 'pegboard', + 'pelican', + 'pellet', + 'pelt', + 'pelvis', + 'penalize', + 'penalty', + 'pencil', + 'pendant', + 'pending', + 'penholder', + 'penknife', + 'pennant', + 'penniless', + 'penny', + 'penpal', + 'pension', + 'pentagon', + 'pentagram', + 'pep', + 'perceive', + 'percent', + 'perch', + 'percolate', + 'perennial', + 'perfected', + 'perfectly', + 'perfume', + 'periscope', + 'perish', + 'perjurer', + 'perjury', + 'perkiness', + 'perky', + 'perm', + 'peroxide', + 'perpetual', + 'perplexed', + 'persecute', + 'persevere', + 'persuaded', + 'persuader', + 'pesky', + 'peso', + 'pessimism', + 'pessimist', + 'pester', + 'pesticide', + 'petal', + 'petite', + 'petition', + 'petri', + 'petroleum', + 'petted', + 'petticoat', + 'pettiness', + 'petty', + 'petunia', + 'phantom', + 'phobia', + 'phoenix', + 'phonebook', + 'phoney', + 'phonics', + 'phoniness', + 'phony', + 'phosphate', + 'photo', + 'phrase', + 'phrasing', + 'placard', + 'placate', + 'placidly', + 'plank', + 'planner', + 'plant', + 'plasma', + 'plaster', + 'plastic', + 'plated', + 'platform', + 'plating', + 'platinum', + 'platonic', + 'platter', + 'platypus', + 'plausible', + 'plausibly', + 'playable', + 'playback', + 'player', + 'playful', + 'playgroup', + 'playhouse', + 'playing', + 'playlist', + 'playmaker', + 'playmate', + 'playoff', + 'playpen', + 'playroom', + 'playset', + 'plaything', + 'playtime', + 'plaza', + 'pleading', + 'pleat', + 'pledge', + 'plentiful', + 'plenty', + 'plethora', + 'plexiglas', + 'pliable', + 'plod', + 'plop', + 'plot', + 'plow', + 'ploy', + 'pluck', + 'plug', + 'plunder', + 'plunging', + 'plural', + 'plus', + 'plutonium', + 'plywood', + 'poach', + 'pod', + 'poem', + 'poet', + 'pogo', + 'pointed', + 'pointer', + 'pointing', + 'pointless', + 'pointy', + 'poise', + 'poison', + 'poker', + 'poking', + 'polar', + 'police', + 'policy', + 'polio', + 'polish', + 'politely', + 'polka', + 'polo', + 'polyester', + 'polygon', + 'polygraph', + 'polymer', + 'poncho', + 'pond', + 'pony', + 'popcorn', + 'pope', + 'poplar', + 'popper', + 'poppy', + 'popsicle', + 'populace', + 'popular', + 'populate', + 'porcupine', + 'pork', + 'porous', + 'porridge', + 'portable', + 'portal', + 'portfolio', + 'porthole', + 'portion', + 'portly', + 'portside', + 'poser', + 'posh', + 'posing', + 'possible', + 'possibly', + 'possum', + 'postage', + 'postal', + 'postbox', + 'postcard', + 'posted', + 'poster', + 'posting', + 'postnasal', + 'posture', + 'postwar', + 'pouch', + 'pounce', + 'pouncing', + 'pound', + 'pouring', + 'pout', + 'powdered', + 'powdering', + 'powdery', + 'power', + 'powwow', + 'pox', + 'praising', + 'prance', + 'prancing', + 'pranker', + 'prankish', + 'prankster', + 'prayer', + 'praying', + 'preacher', + 'preaching', + 'preachy', + 'preamble', + 'precinct', + 'precise', + 'precision', + 'precook', + 'precut', + 'predator', + 'predefine', + 'predict', + 'preface', + 'prefix', + 'preflight', + 'preformed', + 'pregame', + 'pregnancy', + 'pregnant', + 'preheated', + 'prelaunch', + 'prelaw', + 'prelude', + 'premiere', + 'premises', + 'premium', + 'prenatal', + 'preoccupy', + 'preorder', + 'prepaid', + 'prepay', + 'preplan', + 'preppy', + 'preschool', + 'prescribe', + 'preseason', + 'preset', + 'preshow', + 'president', + 'presoak', + 'press', + 'presume', + 'presuming', + 'preteen', + 'pretended', + 'pretender', + 'pretense', + 'pretext', + 'pretty', + 'pretzel', + 'prevail', + 'prevalent', + 'prevent', + 'preview', + 'previous', + 'prewar', + 'prewashed', + 'prideful', + 'pried', + 'primal', + 'primarily', + 'primary', + 'primate', + 'primer', + 'primp', + 'princess', + 'print', + 'prior', + 'prism', + 'prison', + 'prissy', + 'pristine', + 'privacy', + 'private', + 'privatize', + 'prize', + 'proactive', + 'probable', + 'probably', + 'probation', + 'probe', + 'probing', + 'probiotic', + 'problem', + 'procedure', + 'process', + 'proclaim', + 'procreate', + 'procurer', + 'prodigal', + 'prodigy', + 'produce', + 'product', + 'profane', + 'profanity', + 'professed', + 'professor', + 'profile', + 'profound', + 'profusely', + 'progeny', + 'prognosis', + 'program', + 'progress', + 'projector', + 'prologue', + 'prolonged', + 'promenade', + 'prominent', + 'promoter', + 'promotion', + 'prompter', + 'promptly', + 'prone', + 'prong', + 'pronounce', + 'pronto', + 'proofing', + 'proofread', + 'proofs', + 'propeller', + 'properly', + 'property', + 'proponent', + 'proposal', + 'propose', + 'props', + 'prorate', + 'protector', + 'protegee', + 'proton', + 'prototype', + 'protozoan', + 'protract', + 'protrude', + 'proud', + 'provable', + 'proved', + 'proven', + 'provided', + 'provider', + 'providing', + 'province', + 'proving', + 'provoke', + 'provoking', + 'provolone', + 'prowess', + 'prowler', + 'prowling', + 'proximity', + 'proxy', + 'prozac', + 'prude', + 'prudishly', + 'prune', + 'pruning', + 'pry', + 'psychic', + 'public', + 'publisher', + 'pucker', + 'pueblo', + 'pug', + 'pull', + 'pulmonary', + 'pulp', + 'pulsate', + 'pulse', + 'pulverize', + 'puma', + 'pumice', + 'pummel', + 'punch', + 'punctual', + 'punctuate', + 'punctured', + 'pungent', + 'punisher', + 'punk', + 'pupil', + 'puppet', + 'puppy', + 'purchase', + 'pureblood', + 'purebred', + 'purely', + 'pureness', + 'purgatory', + 'purge', + 'purging', + 'purifier', + 'purify', + 'purist', + 'puritan', + 'purity', + 'purple', + 'purplish', + 'purposely', + 'purr', + 'purse', + 'pursuable', + 'pursuant', + 'pursuit', + 'purveyor', + 'pushcart', + 'pushchair', + 'pusher', + 'pushiness', + 'pushing', + 'pushover', + 'pushpin', + 'pushup', + 'pushy', + 'putdown', + 'putt', + 'puzzle', + 'puzzling', + 'pyramid', + 'pyromania', + 'python', + 'quack', + 'quadrant', + 'quail', + 'quaintly', + 'quake', + 'quaking', + 'qualified', + 'qualifier', + 'qualify', + 'quality', + 'qualm', + 'quantum', + 'quarrel', + 'quarry', + 'quartered', + 'quarterly', + 'quarters', + 'quartet', + 'quench', + 'query', + 'quicken', + 'quickly', + 'quickness', + 'quicksand', + 'quickstep', + 'quiet', + 'quill', + 'quilt', + 'quintet', + 'quintuple', + 'quirk', + 'quit', + 'quiver', + 'quizzical', + 'quotable', + 'quotation', + 'quote', + 'rabid', + 'race', + 'racing', + 'racism', + 'rack', + 'racoon', + 'radar', + 'radial', + 'radiance', + 'radiantly', + 'radiated', + 'radiation', + 'radiator', + 'radio', + 'radish', + 'raffle', + 'raft', + 'rage', + 'ragged', + 'raging', + 'ragweed', + 'raider', + 'railcar', + 'railing', + 'railroad', + 'railway', + 'raisin', + 'rake', + 'raking', + 'rally', + 'ramble', + 'rambling', + 'ramp', + 'ramrod', + 'ranch', + 'rancidity', + 'random', + 'ranged', + 'ranger', + 'ranging', + 'ranked', + 'ranking', + 'ransack', + 'ranting', + 'rants', + 'rare', + 'rarity', + 'rascal', + 'rash', + 'rasping', + 'ravage', + 'raven', + 'ravine', + 'raving', + 'ravioli', + 'ravishing', + 'reabsorb', + 'reach', + 'reacquire', + 'reaction', + 'reactive', + 'reactor', + 'reaffirm', + 'ream', + 'reanalyze', + 'reappear', + 'reapply', + 'reappoint', + 'reapprove', + 'rearrange', + 'rearview', + 'reason', + 'reassign', + 'reassure', + 'reattach', + 'reawake', + 'rebalance', + 'rebate', + 'rebel', + 'rebirth', + 'reboot', + 'reborn', + 'rebound', + 'rebuff', + 'rebuild', + 'rebuilt', + 'reburial', + 'rebuttal', + 'recall', + 'recant', + 'recapture', + 'recast', + 'recede', + 'recent', + 'recess', + 'recharger', + 'recipient', + 'recital', + 'recite', + 'reckless', + 'reclaim', + 'recliner', + 'reclining', + 'recluse', + 'reclusive', + 'recognize', + 'recoil', + 'recollect', + 'recolor', + 'reconcile', + 'reconfirm', + 'reconvene', + 'recopy', + 'record', + 'recount', + 'recoup', + 'recovery', + 'recreate', + 'rectal', + 'rectangle', + 'rectified', + 'rectify', + 'recycled', + 'recycler', + 'recycling', + 'reemerge', + 'reenact', + 'reenter', + 'reentry', + 'reexamine', + 'referable', + 'referee', + 'reference', + 'refill', + 'refinance', + 'refined', + 'refinery', + 'refining', + 'refinish', + 'reflected', + 'reflector', + 'reflex', + 'reflux', + 'refocus', + 'refold', + 'reforest', + 'reformat', + 'reformed', + 'reformer', + 'reformist', + 'refract', + 'refrain', + 'refreeze', + 'refresh', + 'refried', + 'refueling', + 'refund', + 'refurbish', + 'refurnish', + 'refusal', + 'refuse', + 'refusing', + 'refutable', + 'refute', + 'regain', + 'regalia', + 'regally', + 'reggae', + 'regime', + 'region', + 'register', + 'registrar', + 'registry', + 'regress', + 'regretful', + 'regroup', + 'regular', + 'regulate', + 'regulator', + 'rehab', + 'reheat', + 'rehire', + 'rehydrate', + 'reimburse', + 'reissue', + 'reiterate', + 'rejoice', + 'rejoicing', + 'rejoin', + 'rekindle', + 'relapse', + 'relapsing', + 'relatable', + 'related', + 'relation', + 'relative', + 'relax', + 'relay', + 'relearn', + 'release', + 'relenting', + 'reliable', + 'reliably', + 'reliance', + 'reliant', + 'relic', + 'relieve', + 'relieving', + 'relight', + 'relish', + 'relive', + 'reload', + 'relocate', + 'relock', + 'reluctant', + 'rely', + 'remake', + 'remark', + 'remarry', + 'rematch', + 'remedial', + 'remedy', + 'remember', + 'reminder', + 'remindful', + 'remission', + 'remix', + 'remnant', + 'remodeler', + 'remold', + 'remorse', + 'remote', + 'removable', + 'removal', + 'removed', + 'remover', + 'removing', + 'rename', + 'renderer', + 'rendering', + 'rendition', + 'renegade', + 'renewable', + 'renewably', + 'renewal', + 'renewed', + 'renounce', + 'renovate', + 'renovator', + 'rentable', + 'rental', + 'rented', + 'renter', + 'reoccupy', + 'reoccur', + 'reopen', + 'reorder', + 'repackage', + 'repacking', + 'repaint', + 'repair', + 'repave', + 'repaying', + 'repayment', + 'repeal', + 'repeated', + 'repeater', + 'repent', + 'rephrase', + 'replace', + 'replay', + 'replica', + 'reply', + 'reporter', + 'repose', + 'repossess', + 'repost', + 'repressed', + 'reprimand', + 'reprint', + 'reprise', + 'reproach', + 'reprocess', + 'reproduce', + 'reprogram', + 'reps', + 'reptile', + 'reptilian', + 'repugnant', + 'repulsion', + 'repulsive', + 'repurpose', + 'reputable', + 'reputably', + 'request', + 'require', + 'requisite', + 'reroute', + 'rerun', + 'resale', + 'resample', + 'rescuer', + 'reseal', + 'research', + 'reselect', + 'reseller', + 'resemble', + 'resend', + 'resent', + 'reset', + 'reshape', + 'reshoot', + 'reshuffle', + 'residence', + 'residency', + 'resident', + 'residual', + 'residue', + 'resigned', + 'resilient', + 'resistant', + 'resisting', + 'resize', + 'resolute', + 'resolved', + 'resonant', + 'resonate', + 'resort', + 'resource', + 'respect', + 'resubmit', + 'result', + 'resume', + 'resupply', + 'resurface', + 'resurrect', + 'retail', + 'retainer', + 'retaining', + 'retake', + 'retaliate', + 'retention', + 'rethink', + 'retinal', + 'retired', + 'retiree', + 'retiring', + 'retold', + 'retool', + 'retorted', + 'retouch', + 'retrace', + 'retract', + 'retrain', + 'retread', + 'retreat', + 'retrial', + 'retrieval', + 'retriever', + 'retry', + 'return', + 'retying', + 'retype', + 'reunion', + 'reunite', + 'reusable', + 'reuse', + 'reveal', + 'reveler', + 'revenge', + 'revenue', + 'reverb', + 'revered', + 'reverence', + 'reverend', + 'reversal', + 'reverse', + 'reversing', + 'reversion', + 'revert', + 'revisable', + 'revise', + 'revision', + 'revisit', + 'revivable', + 'revival', + 'reviver', + 'reviving', + 'revocable', + 'revoke', + 'revolt', + 'revolver', + 'revolving', + 'reward', + 'rewash', + 'rewind', + 'rewire', + 'reword', + 'rework', + 'rewrap', + 'rewrite', + 'rhyme', + 'ribbon', + 'ribcage', + 'rice', + 'riches', + 'richly', + 'richness', + 'rickety', + 'ricotta', + 'riddance', + 'ridden', + 'ride', + 'riding', + 'rifling', + 'rift', + 'rigging', + 'rigid', + 'rigor', + 'rimless', + 'rimmed', + 'rind', + 'rink', + 'rinse', + 'rinsing', + 'riot', + 'ripcord', + 'ripeness', + 'ripening', + 'ripping', + 'ripple', + 'rippling', + 'riptide', + 'rise', + 'rising', + 'risk', + 'risotto', + 'ritalin', + 'ritzy', + 'rival', + 'riverbank', + 'riverbed', + 'riverboat', + 'riverside', + 'riveter', + 'riveting', + 'roamer', + 'roaming', + 'roast', + 'robbing', + 'robe', + 'robin', + 'robotics', + 'robust', + 'rockband', + 'rocker', + 'rocket', + 'rockfish', + 'rockiness', + 'rocking', + 'rocklike', + 'rockslide', + 'rockstar', + 'rocky', + 'rogue', + 'roman', + 'romp', + 'rope', + 'roping', + 'roster', + 'rosy', + 'rotten', + 'rotting', + 'rotunda', + 'roulette', + 'rounding', + 'roundish', + 'roundness', + 'roundup', + 'roundworm', + 'routine', + 'routing', + 'rover', + 'roving', + 'royal', + 'rubbed', + 'rubber', + 'rubbing', + 'rubble', + 'rubdown', + 'ruby', + 'ruckus', + 'rudder', + 'rug', + 'ruined', + 'rule', + 'rumble', + 'rumbling', + 'rummage', + 'rumor', + 'runaround', + 'rundown', + 'runner', + 'running', + 'runny', + 'runt', + 'runway', + 'rupture', + 'rural', + 'ruse', + 'rush', + 'rust', + 'rut', + 'sabbath', + 'sabotage', + 'sacrament', + 'sacred', + 'sacrifice', + 'sadden', + 'saddlebag', + 'saddled', + 'saddling', + 'sadly', + 'sadness', + 'safari', + 'safeguard', + 'safehouse', + 'safely', + 'safeness', + 'saffron', + 'saga', + 'sage', + 'sagging', + 'saggy', + 'said', + 'saint', + 'sake', + 'salad', + 'salami', + 'salaried', + 'salary', + 'saline', + 'salon', + 'saloon', + 'salsa', + 'salt', + 'salutary', + 'salute', + 'salvage', + 'salvaging', + 'salvation', + 'same', + 'sample', + 'sampling', + 'sanction', + 'sanctity', + 'sanctuary', + 'sandal', + 'sandbag', + 'sandbank', + 'sandbar', + 'sandblast', + 'sandbox', + 'sanded', + 'sandfish', + 'sanding', + 'sandlot', + 'sandpaper', + 'sandpit', + 'sandstone', + 'sandstorm', + 'sandworm', + 'sandy', + 'sanitary', + 'sanitizer', + 'sank', + 'santa', + 'sapling', + 'sappiness', + 'sappy', + 'sarcasm', + 'sarcastic', + 'sardine', + 'sash', + 'sasquatch', + 'sassy', + 'satchel', + 'satiable', + 'satin', + 'satirical', + 'satisfied', + 'satisfy', + 'saturate', + 'saturday', + 'sauciness', + 'saucy', + 'sauna', + 'savage', + 'savanna', + 'saved', + 'savings', + 'savior', + 'savor', + 'saxophone', + 'say', + 'scabbed', + 'scabby', + 'scalded', + 'scalding', + 'scale', + 'scaling', + 'scallion', + 'scallop', + 'scalping', + 'scam', + 'scandal', + 'scanner', + 'scanning', + 'scant', + 'scapegoat', + 'scarce', + 'scarcity', + 'scarecrow', + 'scared', + 'scarf', + 'scarily', + 'scariness', + 'scarring', + 'scary', + 'scavenger', + 'scenic', + 'schedule', + 'schematic', + 'scheme', + 'scheming', + 'schilling', + 'schnapps', + 'scholar', + 'science', + 'scientist', + 'scion', + 'scoff', + 'scolding', + 'scone', + 'scoop', + 'scooter', + 'scope', + 'scorch', + 'scorebook', + 'scorecard', + 'scored', + 'scoreless', + 'scorer', + 'scoring', + 'scorn', + 'scorpion', + 'scotch', + 'scoundrel', + 'scoured', + 'scouring', + 'scouting', + 'scouts', + 'scowling', + 'scrabble', + 'scraggly', + 'scrambled', + 'scrambler', + 'scrap', + 'scratch', + 'scrawny', + 'screen', + 'scribble', + 'scribe', + 'scribing', + 'scrimmage', + 'script', + 'scroll', + 'scrooge', + 'scrounger', + 'scrubbed', + 'scrubber', + 'scruffy', + 'scrunch', + 'scrutiny', + 'scuba', + 'scuff', + 'sculptor', + 'sculpture', + 'scurvy', + 'scuttle', + 'secluded', + 'secluding', + 'seclusion', + 'second', + 'secrecy', + 'secret', + 'sectional', + 'sector', + 'secular', + 'securely', + 'security', + 'sedan', + 'sedate', + 'sedation', + 'sedative', + 'sediment', + 'seduce', + 'seducing', + 'segment', + 'seismic', + 'seizing', + 'seldom', + 'selected', + 'selection', + 'selective', + 'selector', + 'self', + 'seltzer', + 'semantic', + 'semester', + 'semicolon', + 'semifinal', + 'seminar', + 'semisoft', + 'semisweet', + 'senate', + 'senator', + 'send', + 'senior', + 'senorita', + 'sensation', + 'sensitive', + 'sensitize', + 'sensually', + 'sensuous', + 'sepia', + 'september', + 'septic', + 'septum', + 'sequel', + 'sequence', + 'sequester', + 'series', + 'sermon', + 'serotonin', + 'serpent', + 'serrated', + 'serve', + 'service', + 'serving', + 'sesame', + 'sessions', + 'setback', + 'setting', + 'settle', + 'settling', + 'setup', + 'sevenfold', + 'seventeen', + 'seventh', + 'seventy', + 'severity', + 'shabby', + 'shack', + 'shaded', + 'shadily', + 'shadiness', + 'shading', + 'shadow', + 'shady', + 'shaft', + 'shakable', + 'shakily', + 'shakiness', + 'shaking', + 'shaky', + 'shale', + 'shallot', + 'shallow', + 'shame', + 'shampoo', + 'shamrock', + 'shank', + 'shanty', + 'shape', + 'shaping', + 'share', + 'sharpener', + 'sharper', + 'sharpie', + 'sharply', + 'sharpness', + 'shawl', + 'sheath', + 'shed', + 'sheep', + 'sheet', + 'shelf', + 'shell', + 'shelter', + 'shelve', + 'shelving', + 'sherry', + 'shield', + 'shifter', + 'shifting', + 'shiftless', + 'shifty', + 'shimmer', + 'shimmy', + 'shindig', + 'shine', + 'shingle', + 'shininess', + 'shining', + 'shiny', + 'ship', + 'shirt', + 'shivering', + 'shock', + 'shone', + 'shoplift', + 'shopper', + 'shopping', + 'shoptalk', + 'shore', + 'shortage', + 'shortcake', + 'shortcut', + 'shorten', + 'shorter', + 'shorthand', + 'shortlist', + 'shortly', + 'shortness', + 'shorts', + 'shortwave', + 'shorty', + 'shout', + 'shove', + 'showbiz', + 'showcase', + 'showdown', + 'shower', + 'showgirl', + 'showing', + 'showman', + 'shown', + 'showoff', + 'showpiece', + 'showplace', + 'showroom', + 'showy', + 'shrank', + 'shrapnel', + 'shredder', + 'shredding', + 'shrewdly', + 'shriek', + 'shrill', + 'shrimp', + 'shrine', + 'shrink', + 'shrivel', + 'shrouded', + 'shrubbery', + 'shrubs', + 'shrug', + 'shrunk', + 'shucking', + 'shudder', + 'shuffle', + 'shuffling', + 'shun', + 'shush', + 'shut', + 'shy', + 'siamese', + 'siberian', + 'sibling', + 'siding', + 'sierra', + 'siesta', + 'sift', + 'sighing', + 'silenced', + 'silencer', + 'silent', + 'silica', + 'silicon', + 'silk', + 'silliness', + 'silly', + 'silo', + 'silt', + 'silver', + 'similarly', + 'simile', + 'simmering', + 'simple', + 'simplify', + 'simply', + 'sincere', + 'sincerity', + 'singer', + 'singing', + 'single', + 'singular', + 'sinister', + 'sinless', + 'sinner', + 'sinuous', + 'sip', + 'siren', + 'sister', + 'sitcom', + 'sitter', + 'sitting', + 'situated', + 'situation', + 'sixfold', + 'sixteen', + 'sixth', + 'sixties', + 'sixtieth', + 'sixtyfold', + 'sizable', + 'sizably', + 'size', + 'sizing', + 'sizzle', + 'sizzling', + 'skater', + 'skating', + 'skedaddle', + 'skeletal', + 'skeleton', + 'skeptic', + 'sketch', + 'skewed', + 'skewer', + 'skid', + 'skied', + 'skier', + 'skies', + 'skiing', + 'skilled', + 'skillet', + 'skillful', + 'skimmed', + 'skimmer', + 'skimming', + 'skimpily', + 'skincare', + 'skinhead', + 'skinless', + 'skinning', + 'skinny', + 'skintight', + 'skipper', + 'skipping', + 'skirmish', + 'skirt', + 'skittle', + 'skydiver', + 'skylight', + 'skyline', + 'skype', + 'skyrocket', + 'skyward', + 'slab', + 'slacked', + 'slacker', + 'slacking', + 'slackness', + 'slacks', + 'slain', + 'slam', + 'slander', + 'slang', + 'slapping', + 'slapstick', + 'slashed', + 'slashing', + 'slate', + 'slather', + 'slaw', + 'sled', + 'sleek', + 'sleep', + 'sleet', + 'sleeve', + 'slept', + 'sliceable', + 'sliced', + 'slicer', + 'slicing', + 'slick', + 'slider', + 'slideshow', + 'sliding', + 'slighted', + 'slighting', + 'slightly', + 'slimness', + 'slimy', + 'slinging', + 'slingshot', + 'slinky', + 'slip', + 'slit', + 'sliver', + 'slobbery', + 'slogan', + 'sloped', + 'sloping', + 'sloppily', + 'sloppy', + 'slot', + 'slouching', + 'slouchy', + 'sludge', + 'slug', + 'slum', + 'slurp', + 'slush', + 'sly', + 'small', + 'smartly', + 'smartness', + 'smasher', + 'smashing', + 'smashup', + 'smell', + 'smelting', + 'smile', + 'smilingly', + 'smirk', + 'smite', + 'smith', + 'smitten', + 'smock', + 'smog', + 'smoked', + 'smokeless', + 'smokiness', + 'smoking', + 'smoky', + 'smolder', + 'smooth', + 'smother', + 'smudge', + 'smudgy', + 'smuggler', + 'smuggling', + 'smugly', + 'smugness', + 'snack', + 'snagged', + 'snaking', + 'snap', + 'snare', + 'snarl', + 'snazzy', + 'sneak', + 'sneer', + 'sneeze', + 'sneezing', + 'snide', + 'sniff', + 'snippet', + 'snipping', + 'snitch', + 'snooper', + 'snooze', + 'snore', + 'snoring', + 'snorkel', + 'snort', + 'snout', + 'snowbird', + 'snowboard', + 'snowbound', + 'snowcap', + 'snowdrift', + 'snowdrop', + 'snowfall', + 'snowfield', + 'snowflake', + 'snowiness', + 'snowless', + 'snowman', + 'snowplow', + 'snowshoe', + 'snowstorm', + 'snowsuit', + 'snowy', + 'snub', + 'snuff', + 'snuggle', + 'snugly', + 'snugness', + 'speak', + 'spearfish', + 'spearhead', + 'spearman', + 'spearmint', + 'species', + 'specimen', + 'specked', + 'speckled', + 'specks', + 'spectacle', + 'spectator', + 'spectrum', + 'speculate', + 'speech', + 'speed', + 'spellbind', + 'speller', + 'spelling', + 'spendable', + 'spender', + 'spending', + 'spent', + 'spew', + 'sphere', + 'spherical', + 'sphinx', + 'spider', + 'spied', + 'spiffy', + 'spill', + 'spilt', + 'spinach', + 'spinal', + 'spindle', + 'spinner', + 'spinning', + 'spinout', + 'spinster', + 'spiny', + 'spiral', + 'spirited', + 'spiritism', + 'spirits', + 'spiritual', + 'splashed', + 'splashing', + 'splashy', + 'splatter', + 'spleen', + 'splendid', + 'splendor', + 'splice', + 'splicing', + 'splinter', + 'splotchy', + 'splurge', + 'spoilage', + 'spoiled', + 'spoiler', + 'spoiling', + 'spoils', + 'spoken', + 'spokesman', + 'sponge', + 'spongy', + 'sponsor', + 'spoof', + 'spookily', + 'spooky', + 'spool', + 'spoon', + 'spore', + 'sporting', + 'sports', + 'sporty', + 'spotless', + 'spotlight', + 'spotted', + 'spotter', + 'spotting', + 'spotty', + 'spousal', + 'spouse', + 'spout', + 'sprain', + 'sprang', + 'sprawl', + 'spray', + 'spree', + 'sprig', + 'spring', + 'sprinkled', + 'sprinkler', + 'sprint', + 'sprite', + 'sprout', + 'spruce', + 'sprung', + 'spry', + 'spud', + 'spur', + 'sputter', + 'spyglass', + 'squabble', + 'squad', + 'squall', + 'squander', + 'squash', + 'squatted', + 'squatter', + 'squatting', + 'squeak', + 'squealer', + 'squealing', + 'squeamish', + 'squeegee', + 'squeeze', + 'squeezing', + 'squid', + 'squiggle', + 'squiggly', + 'squint', + 'squire', + 'squirt', + 'squishier', + 'squishy', + 'stability', + 'stabilize', + 'stable', + 'stack', + 'stadium', + 'staff', + 'stage', + 'staging', + 'stagnant', + 'stagnate', + 'stainable', + 'stained', + 'staining', + 'stainless', + 'stalemate', + 'staleness', + 'stalling', + 'stallion', + 'stamina', + 'stammer', + 'stamp', + 'stand', + 'stank', + 'staple', + 'stapling', + 'starboard', + 'starch', + 'stardom', + 'stardust', + 'starfish', + 'stargazer', + 'staring', + 'stark', + 'starless', + 'starlet', + 'starlight', + 'starlit', + 'starring', + 'starry', + 'starship', + 'starter', + 'starting', + 'startle', + 'startling', + 'startup', + 'starved', + 'starving', + 'stash', + 'state', + 'static', + 'statistic', + 'statue', + 'stature', + 'status', + 'statute', + 'statutory', + 'staunch', + 'stays', + 'steadfast', + 'steadier', + 'steadily', + 'steadying', + 'steam', + 'steed', + 'steep', + 'steerable', + 'steering', + 'steersman', + 'stegosaur', + 'stellar', + 'stem', + 'stench', + 'stencil', + 'step', + 'stereo', + 'sterile', + 'sterility', + 'sterilize', + 'sterling', + 'sternness', + 'sternum', + 'stew', + 'stick', + 'stiffen', + 'stiffly', + 'stiffness', + 'stifle', + 'stifling', + 'stillness', + 'stilt', + 'stimulant', + 'stimulate', + 'stimuli', + 'stimulus', + 'stinger', + 'stingily', + 'stinging', + 'stingray', + 'stingy', + 'stinking', + 'stinky', + 'stipend', + 'stipulate', + 'stir', + 'stitch', + 'stock', + 'stoic', + 'stoke', + 'stole', + 'stomp', + 'stonewall', + 'stoneware', + 'stonework', + 'stoning', + 'stony', + 'stood', + 'stooge', + 'stool', + 'stoop', + 'stoplight', + 'stoppable', + 'stoppage', + 'stopped', + 'stopper', + 'stopping', + 'stopwatch', + 'storable', + 'storage', + 'storeroom', + 'storewide', + 'storm', + 'stout', + 'stove', + 'stowaway', + 'stowing', + 'straddle', + 'straggler', + 'strained', + 'strainer', + 'straining', + 'strangely', + 'stranger', + 'strangle', + 'strategic', + 'strategy', + 'stratus', + 'straw', + 'stray', + 'streak', + 'stream', + 'street', + 'strength', + 'strenuous', + 'strep', + 'stress', + 'stretch', + 'strewn', + 'stricken', + 'strict', + 'stride', + 'strife', + 'strike', + 'striking', + 'strive', + 'striving', + 'strobe', + 'strode', + 'stroller', + 'strongbox', + 'strongly', + 'strongman', + 'struck', + 'structure', + 'strudel', + 'struggle', + 'strum', + 'strung', + 'strut', + 'stubbed', + 'stubble', + 'stubbly', + 'stubborn', + 'stucco', + 'stuck', + 'student', + 'studied', + 'studio', + 'study', + 'stuffed', + 'stuffing', + 'stuffy', + 'stumble', + 'stumbling', + 'stump', + 'stung', + 'stunned', + 'stunner', + 'stunning', + 'stunt', + 'stupor', + 'sturdily', + 'sturdy', + 'styling', + 'stylishly', + 'stylist', + 'stylized', + 'stylus', + 'suave', + 'subarctic', + 'subatomic', + 'subdivide', + 'subdued', + 'subduing', + 'subfloor', + 'subgroup', + 'subheader', + 'subject', + 'sublease', + 'sublet', + 'sublevel', + 'sublime', + 'submarine', + 'submerge', + 'submersed', + 'submitter', + 'subpanel', + 'subpar', + 'subplot', + 'subprime', + 'subscribe', + 'subscript', + 'subsector', + 'subside', + 'subsiding', + 'subsidize', + 'subsidy', + 'subsoil', + 'subsonic', + 'substance', + 'subsystem', + 'subtext', + 'subtitle', + 'subtly', + 'subtotal', + 'subtract', + 'subtype', + 'suburb', + 'subway', + 'subwoofer', + 'subzero', + 'succulent', + 'such', + 'suction', + 'sudden', + 'sudoku', + 'suds', + 'sufferer', + 'suffering', + 'suffice', + 'suffix', + 'suffocate', + 'suffrage', + 'sugar', + 'suggest', + 'suing', + 'suitable', + 'suitably', + 'suitcase', + 'suitor', + 'sulfate', + 'sulfide', + 'sulfite', + 'sulfur', + 'sulk', + 'sullen', + 'sulphate', + 'sulphuric', + 'sultry', + 'superbowl', + 'superglue', + 'superhero', + 'superior', + 'superjet', + 'superman', + 'supermom', + 'supernova', + 'supervise', + 'supper', + 'supplier', + 'supply', + 'support', + 'supremacy', + 'supreme', + 'surcharge', + 'surely', + 'sureness', + 'surface', + 'surfacing', + 'surfboard', + 'surfer', + 'surgery', + 'surgical', + 'surging', + 'surname', + 'surpass', + 'surplus', + 'surprise', + 'surreal', + 'surrender', + 'surrogate', + 'surround', + 'survey', + 'survival', + 'survive', + 'surviving', + 'survivor', + 'sushi', + 'suspect', + 'suspend', + 'suspense', + 'sustained', + 'sustainer', + 'swab', + 'swaddling', + 'swagger', + 'swampland', + 'swan', + 'swapping', + 'swarm', + 'sway', + 'swear', + 'sweat', + 'sweep', + 'swell', + 'swept', + 'swerve', + 'swifter', + 'swiftly', + 'swiftness', + 'swimmable', + 'swimmer', + 'swimming', + 'swimsuit', + 'swimwear', + 'swinger', + 'swinging', + 'swipe', + 'swirl', + 'switch', + 'swivel', + 'swizzle', + 'swooned', + 'swoop', + 'swoosh', + 'swore', + 'sworn', + 'swung', + 'sycamore', + 'sympathy', + 'symphonic', + 'symphony', + 'symptom', + 'synapse', + 'syndrome', + 'synergy', + 'synopses', + 'synopsis', + 'synthesis', + 'synthetic', + 'syrup', + 'system', + 't-shirt', + 'tabasco', + 'tabby', + 'tableful', + 'tables', + 'tablet', + 'tableware', + 'tabloid', + 'tackiness', + 'tacking', + 'tackle', + 'tackling', + 'tacky', + 'taco', + 'tactful', + 'tactical', + 'tactics', + 'tactile', + 'tactless', + 'tadpole', + 'taekwondo', + 'tag', + 'tainted', + 'take', + 'taking', + 'talcum', + 'talisman', + 'tall', + 'talon', + 'tamale', + 'tameness', + 'tamer', + 'tamper', + 'tank', + 'tanned', + 'tannery', + 'tanning', + 'tantrum', + 'tapeless', + 'tapered', + 'tapering', + 'tapestry', + 'tapioca', + 'tapping', + 'taps', + 'tarantula', + 'target', + 'tarmac', + 'tarnish', + 'tarot', + 'tartar', + 'tartly', + 'tartness', + 'task', + 'tassel', + 'taste', + 'tastiness', + 'tasting', + 'tasty', + 'tattered', + 'tattle', + 'tattling', + 'tattoo', + 'taunt', + 'tavern', + 'thank', + 'that', + 'thaw', + 'theater', + 'theatrics', + 'thee', + 'theft', + 'theme', + 'theology', + 'theorize', + 'thermal', + 'thermos', + 'thesaurus', + 'these', + 'thesis', + 'thespian', + 'thicken', + 'thicket', + 'thickness', + 'thieving', + 'thievish', + 'thigh', + 'thimble', + 'thing', + 'think', + 'thinly', + 'thinner', + 'thinness', + 'thinning', + 'thirstily', + 'thirsting', + 'thirsty', + 'thirteen', + 'thirty', + 'thong', + 'thorn', + 'those', + 'thousand', + 'thrash', + 'thread', + 'threaten', + 'threefold', + 'thrift', + 'thrill', + 'thrive', + 'thriving', + 'throat', + 'throbbing', + 'throng', + 'throttle', + 'throwaway', + 'throwback', + 'thrower', + 'throwing', + 'thud', + 'thumb', + 'thumping', + 'thursday', + 'thus', + 'thwarting', + 'thyself', + 'tiara', + 'tibia', + 'tidal', + 'tidbit', + 'tidiness', + 'tidings', + 'tidy', + 'tiger', + 'tighten', + 'tightly', + 'tightness', + 'tightrope', + 'tightwad', + 'tigress', + 'tile', + 'tiling', + 'till', + 'tilt', + 'timid', + 'timing', + 'timothy', + 'tinderbox', + 'tinfoil', + 'tingle', + 'tingling', + 'tingly', + 'tinker', + 'tinkling', + 'tinsel', + 'tinsmith', + 'tint', + 'tinwork', + 'tiny', + 'tipoff', + 'tipped', + 'tipper', + 'tipping', + 'tiptoeing', + 'tiptop', + 'tiring', + 'tissue', + 'trace', + 'tracing', + 'track', + 'traction', + 'tractor', + 'trade', + 'trading', + 'tradition', + 'traffic', + 'tragedy', + 'trailing', + 'trailside', + 'train', + 'traitor', + 'trance', + 'tranquil', + 'transfer', + 'transform', + 'translate', + 'transpire', + 'transport', + 'transpose', + 'trapdoor', + 'trapeze', + 'trapezoid', + 'trapped', + 'trapper', + 'trapping', + 'traps', + 'trash', + 'travel', + 'traverse', + 'travesty', + 'tray', + 'treachery', + 'treading', + 'treadmill', + 'treason', + 'treat', + 'treble', + 'tree', + 'trekker', + 'tremble', + 'trembling', + 'tremor', + 'trench', + 'trend', + 'trespass', + 'triage', + 'trial', + 'triangle', + 'tribesman', + 'tribunal', + 'tribune', + 'tributary', + 'tribute', + 'triceps', + 'trickery', + 'trickily', + 'tricking', + 'trickle', + 'trickster', + 'tricky', + 'tricolor', + 'tricycle', + 'trident', + 'tried', + 'trifle', + 'trifocals', + 'trillion', + 'trilogy', + 'trimester', + 'trimmer', + 'trimming', + 'trimness', + 'trinity', + 'trio', + 'tripod', + 'tripping', + 'triumph', + 'trivial', + 'trodden', + 'trolling', + 'trombone', + 'trophy', + 'tropical', + 'tropics', + 'trouble', + 'troubling', + 'trough', + 'trousers', + 'trout', + 'trowel', + 'truce', + 'truck', + 'truffle', + 'trump', + 'trunks', + 'trustable', + 'trustee', + 'trustful', + 'trusting', + 'trustless', + 'truth', + 'try', + 'tubby', + 'tubeless', + 'tubular', + 'tucking', + 'tuesday', + 'tug', + 'tuition', + 'tulip', + 'tumble', + 'tumbling', + 'tummy', + 'turban', + 'turbine', + 'turbofan', + 'turbojet', + 'turbulent', + 'turf', + 'turkey', + 'turmoil', + 'turret', + 'turtle', + 'tusk', + 'tutor', + 'tutu', + 'tux', + 'tweak', + 'tweed', + 'tweet', + 'tweezers', + 'twelve', + 'twentieth', + 'twenty', + 'twerp', + 'twice', + 'twiddle', + 'twiddling', + 'twig', + 'twilight', + 'twine', + 'twins', + 'twirl', + 'twistable', + 'twisted', + 'twister', + 'twisting', + 'twisty', + 'twitch', + 'twitter', + 'tycoon', + 'tying', + 'tyke', + 'udder', + 'ultimate', + 'ultimatum', + 'ultra', + 'umbilical', + 'umbrella', + 'umpire', + 'unabashed', + 'unable', + 'unadorned', + 'unadvised', + 'unafraid', + 'unaired', + 'unaligned', + 'unaltered', + 'unarmored', + 'unashamed', + 'unaudited', + 'unawake', + 'unaware', + 'unbaked', + 'unbalance', + 'unbeaten', + 'unbend', + 'unbent', + 'unbiased', + 'unbitten', + 'unblended', + 'unblessed', + 'unblock', + 'unbolted', + 'unbounded', + 'unboxed', + 'unbraided', + 'unbridle', + 'unbroken', + 'unbuckled', + 'unbundle', + 'unburned', + 'unbutton', + 'uncanny', + 'uncapped', + 'uncaring', + 'uncertain', + 'unchain', + 'unchanged', + 'uncharted', + 'uncheck', + 'uncivil', + 'unclad', + 'unclaimed', + 'unclamped', + 'unclasp', + 'uncle', + 'unclip', + 'uncloak', + 'unclog', + 'unclothed', + 'uncoated', + 'uncoiled', + 'uncolored', + 'uncombed', + 'uncommon', + 'uncooked', + 'uncork', + 'uncorrupt', + 'uncounted', + 'uncouple', + 'uncouth', + 'uncover', + 'uncross', + 'uncrown', + 'uncrushed', + 'uncured', + 'uncurious', + 'uncurled', + 'uncut', + 'undamaged', + 'undated', + 'undaunted', + 'undead', + 'undecided', + 'undefined', + 'underage', + 'underarm', + 'undercoat', + 'undercook', + 'undercut', + 'underdog', + 'underdone', + 'underfed', + 'underfeed', + 'underfoot', + 'undergo', + 'undergrad', + 'underhand', + 'underline', + 'underling', + 'undermine', + 'undermost', + 'underpaid', + 'underpass', + 'underpay', + 'underrate', + 'undertake', + 'undertone', + 'undertook', + 'undertow', + 'underuse', + 'underwear', + 'underwent', + 'underwire', + 'undesired', + 'undiluted', + 'undivided', + 'undocked', + 'undoing', + 'undone', + 'undrafted', + 'undress', + 'undrilled', + 'undusted', + 'undying', + 'unearned', + 'unearth', + 'unease', + 'uneasily', + 'uneasy', + 'uneatable', + 'uneaten', + 'unedited', + 'unelected', + 'unending', + 'unengaged', + 'unenvied', + 'unequal', + 'unethical', + 'uneven', + 'unexpired', + 'unexposed', + 'unfailing', + 'unfair', + 'unfasten', + 'unfazed', + 'unfeeling', + 'unfiled', + 'unfilled', + 'unfitted', + 'unfitting', + 'unfixable', + 'unfixed', + 'unflawed', + 'unfocused', + 'unfold', + 'unfounded', + 'unframed', + 'unfreeze', + 'unfrosted', + 'unfrozen', + 'unfunded', + 'unglazed', + 'ungloved', + 'unglue', + 'ungodly', + 'ungraded', + 'ungreased', + 'unguarded', + 'unguided', + 'unhappily', + 'unhappy', + 'unharmed', + 'unhealthy', + 'unheard', + 'unhearing', + 'unheated', + 'unhelpful', + 'unhidden', + 'unhinge', + 'unhitched', + 'unholy', + 'unhook', + 'unicorn', + 'unicycle', + 'unified', + 'unifier', + 'uniformed', + 'uniformly', + 'unify', + 'unimpeded', + 'uninjured', + 'uninstall', + 'uninsured', + 'uninvited', + 'union', + 'uniquely', + 'unisexual', + 'unison', + 'unissued', + 'unit', + 'universal', + 'universe', + 'unjustly', + 'unkempt', + 'unkind', + 'unknotted', + 'unknowing', + 'unknown', + 'unlaced', + 'unlatch', + 'unlawful', + 'unleaded', + 'unlearned', + 'unleash', + 'unless', + 'unleveled', + 'unlighted', + 'unlikable', + 'unlimited', + 'unlined', + 'unlinked', + 'unlisted', + 'unlit', + 'unlivable', + 'unloaded', + 'unloader', + 'unlocked', + 'unlocking', + 'unlovable', + 'unloved', + 'unlovely', + 'unloving', + 'unluckily', + 'unlucky', + 'unmade', + 'unmanaged', + 'unmanned', + 'unmapped', + 'unmarked', + 'unmasked', + 'unmasking', + 'unmatched', + 'unmindful', + 'unmixable', + 'unmixed', + 'unmolded', + 'unmoral', + 'unmovable', + 'unmoved', + 'unmoving', + 'unnamable', + 'unnamed', + 'unnatural', + 'unneeded', + 'unnerve', + 'unnerving', + 'unnoticed', + 'unopened', + 'unopposed', + 'unpack', + 'unpadded', + 'unpaid', + 'unpainted', + 'unpaired', + 'unpaved', + 'unpeeled', + 'unpicked', + 'unpiloted', + 'unpinned', + 'unplanned', + 'unplanted', + 'unpleased', + 'unpledged', + 'unplowed', + 'unplug', + 'unpopular', + 'unproven', + 'unquote', + 'unranked', + 'unrated', + 'unraveled', + 'unreached', + 'unread', + 'unreal', + 'unreeling', + 'unrefined', + 'unrelated', + 'unrented', + 'unrest', + 'unretired', + 'unrevised', + 'unrigged', + 'unripe', + 'unrivaled', + 'unroasted', + 'unrobed', + 'unroll', + 'unruffled', + 'unruly', + 'unrushed', + 'unsaddle', + 'unsafe', + 'unsaid', + 'unsalted', + 'unsaved', + 'unsavory', + 'unscathed', + 'unscented', + 'unscrew', + 'unsealed', + 'unseated', + 'unsecured', + 'unseeing', + 'unseemly', + 'unseen', + 'unselect', + 'unselfish', + 'unsent', + 'unsettled', + 'unshackle', + 'unshaken', + 'unshaved', + 'unshaven', + 'unsheathe', + 'unshipped', + 'unsightly', + 'unsigned', + 'unskilled', + 'unsliced', + 'unsmooth', + 'unsnap', + 'unsocial', + 'unsoiled', + 'unsold', + 'unsolved', + 'unsorted', + 'unspoiled', + 'unspoken', + 'unstable', + 'unstaffed', + 'unstamped', + 'unsteady', + 'unsterile', + 'unstirred', + 'unstitch', + 'unstopped', + 'unstuck', + 'unstuffed', + 'unstylish', + 'unsubtle', + 'unsubtly', + 'unsuited', + 'unsure', + 'unsworn', + 'untagged', + 'untainted', + 'untaken', + 'untamed', + 'untangled', + 'untapped', + 'untaxed', + 'unthawed', + 'unthread', + 'untidy', + 'untie', + 'until', + 'untimed', + 'untimely', + 'untitled', + 'untoasted', + 'untold', + 'untouched', + 'untracked', + 'untrained', + 'untreated', + 'untried', + 'untrimmed', + 'untrue', + 'untruth', + 'unturned', + 'untwist', + 'untying', + 'unusable', + 'unused', + 'unusual', + 'unvalued', + 'unvaried', + 'unvarying', + 'unveiled', + 'unveiling', + 'unvented', + 'unviable', + 'unvisited', + 'unvocal', + 'unwanted', + 'unwarlike', + 'unwary', + 'unwashed', + 'unwatched', + 'unweave', + 'unwed', + 'unwelcome', + 'unwell', + 'unwieldy', + 'unwilling', + 'unwind', + 'unwired', + 'unwitting', + 'unwomanly', + 'unworldly', + 'unworn', + 'unworried', + 'unworthy', + 'unwound', + 'unwoven', + 'unwrapped', + 'unwritten', + 'unzip', + 'upbeat', + 'upchuck', + 'upcoming', + 'upcountry', + 'update', + 'upfront', + 'upgrade', + 'upheaval', + 'upheld', + 'uphill', + 'uphold', + 'uplifted', + 'uplifting', + 'upload', + 'upon', + 'upper', + 'upright', + 'uprising', + 'upriver', + 'uproar', + 'uproot', + 'upscale', + 'upside', + 'upstage', + 'upstairs', + 'upstart', + 'upstate', + 'upstream', + 'upstroke', + 'upswing', + 'uptake', + 'uptight', + 'uptown', + 'upturned', + 'upward', + 'upwind', + 'uranium', + 'urban', + 'urchin', + 'urethane', + 'urgency', + 'urgent', + 'urging', + 'urologist', + 'urology', + 'usable', + 'usage', + 'useable', + 'used', + 'uselessly', + 'user', + 'usher', + 'usual', + 'utensil', + 'utility', + 'utilize', + 'utmost', + 'utopia', + 'utter', + 'vacancy', + 'vacant', + 'vacate', + 'vacation', + 'vagabond', + 'vagrancy', + 'vagrantly', + 'vaguely', + 'vagueness', + 'valiant', + 'valid', + 'valium', + 'valley', + 'valuables', + 'value', + 'vanilla', + 'vanish', + 'vanity', + 'vanquish', + 'vantage', + 'vaporizer', + 'variable', + 'variably', + 'varied', + 'variety', + 'various', + 'varmint', + 'varnish', + 'varsity', + 'varying', + 'vascular', + 'vaseline', + 'vastly', + 'vastness', + 'veal', + 'vegan', + 'veggie', + 'vehicular', + 'velcro', + 'velocity', + 'velvet', + 'vendetta', + 'vending', + 'vendor', + 'veneering', + 'vengeful', + 'venomous', + 'ventricle', + 'venture', + 'venue', + 'venus', + 'verbalize', + 'verbally', + 'verbose', + 'verdict', + 'verify', + 'verse', + 'version', + 'versus', + 'vertebrae', + 'vertical', + 'vertigo', + 'very', + 'vessel', + 'vest', + 'veteran', + 'veto', + 'vexingly', + 'viability', + 'viable', + 'vibes', + 'vice', + 'vicinity', + 'victory', + 'video', + 'viewable', + 'viewer', + 'viewing', + 'viewless', + 'viewpoint', + 'vigorous', + 'village', + 'villain', + 'vindicate', + 'vineyard', + 'vintage', + 'violate', + 'violation', + 'violator', + 'violet', + 'violin', + 'viper', + 'viral', + 'virtual', + 'virtuous', + 'virus', + 'visa', + 'viscosity', + 'viscous', + 'viselike', + 'visible', + 'visibly', + 'vision', + 'visiting', + 'visitor', + 'visor', + 'vista', + 'vitality', + 'vitalize', + 'vitally', + 'vitamins', + 'vivacious', + 'vividly', + 'vividness', + 'vixen', + 'vocalist', + 'vocalize', + 'vocally', + 'vocation', + 'voice', + 'voicing', + 'void', + 'volatile', + 'volley', + 'voltage', + 'volumes', + 'voter', + 'voting', + 'voucher', + 'vowed', + 'vowel', + 'voyage', + 'wackiness', + 'wad', + 'wafer', + 'waffle', + 'waged', + 'wager', + 'wages', + 'waggle', + 'wagon', + 'wake', + 'waking', + 'walk', + 'walmart', + 'walnut', + 'walrus', + 'waltz', + 'wand', + 'wannabe', + 'wanted', + 'wanting', + 'wasabi', + 'washable', + 'washbasin', + 'washboard', + 'washbowl', + 'washcloth', + 'washday', + 'washed', + 'washer', + 'washhouse', + 'washing', + 'washout', + 'washroom', + 'washstand', + 'washtub', + 'wasp', + 'wasting', + 'watch', + 'water', + 'waviness', + 'waving', + 'wavy', + 'whacking', + 'whacky', + 'wham', + 'wharf', + 'wheat', + 'whenever', + 'whiff', + 'whimsical', + 'whinny', + 'whiny', + 'whisking', + 'whoever', + 'whole', + 'whomever', + 'whoopee', + 'whooping', + 'whoops', + 'why', + 'wick', + 'widely', + 'widen', + 'widget', + 'widow', + 'width', + 'wieldable', + 'wielder', + 'wife', + 'wifi', + 'wikipedia', + 'wildcard', + 'wildcat', + 'wilder', + 'wildfire', + 'wildfowl', + 'wildland', + 'wildlife', + 'wildly', + 'wildness', + 'willed', + 'willfully', + 'willing', + 'willow', + 'willpower', + 'wilt', + 'wimp', + 'wince', + 'wincing', + 'wind', + 'wing', + 'winking', + 'winner', + 'winnings', + 'winter', + 'wipe', + 'wired', + 'wireless', + 'wiring', + 'wiry', + 'wisdom', + 'wise', + 'wish', + 'wisplike', + 'wispy', + 'wistful', + 'wizard', + 'wobble', + 'wobbling', + 'wobbly', + 'wok', + 'wolf', + 'wolverine', + 'womanhood', + 'womankind', + 'womanless', + 'womanlike', + 'womanly', + 'womb', + 'woof', + 'wooing', + 'wool', + 'woozy', + 'word', + 'work', + 'worried', + 'worrier', + 'worrisome', + 'worry', + 'worsening', + 'worshiper', + 'worst', + 'wound', + 'woven', + 'wow', + 'wrangle', + 'wrath', + 'wreath', + 'wreckage', + 'wrecker', + 'wrecking', + 'wrench', + 'wriggle', + 'wriggly', + 'wrinkle', + 'wrinkly', + 'wrist', + 'writing', + 'written', + 'wrongdoer', + 'wronged', + 'wrongful', + 'wrongly', + 'wrongness', + 'wrought', + 'xbox', + 'xerox', + 'yahoo', + 'yam', + 'yanking', + 'yapping', + 'yard', + 'yarn', + 'yeah', + 'yearbook', + 'yearling', + 'yearly', + 'yearning', + 'yeast', + 'yelling', + 'yelp', + 'yen', + 'yesterday', + 'yiddish', + 'yield', + 'yin', + 'yippee', + 'yo-yo', + 'yodel', + 'yoga', + 'yogurt', + 'yonder', + 'yoyo', + 'yummy', + 'zap', + 'zealous', + 'zebra', + 'zen', + 'zeppelin', + 'zero', + 'zestfully', + 'zesty', + 'zigzagged', + 'zipfile', + 'zipping', + 'zippy', + 'zips', + 'zit', + 'zodiac', + 'zombie', + 'zone', + 'zoning', + 'zookeeper', + 'zoologist', + 'zoology', + 'zoom', + ]; -class PasswordGenerator -{ - private array $words = [ - 'abacus', - 'abdomen', - 'abdominal', - 'abide', - 'abiding', - 'ability', - 'ablaze', - 'able', - 'abnormal', - 'abrasion', - 'abrasive', - 'abreast', - 'abridge', - 'abroad', - 'abruptly', - 'absence', - 'absentee', - 'absently', - 'absinthe', - 'absolute', - 'absolve', - 'abstain', - 'abstract', - 'absurd', - 'accent', - 'acclaim', - 'acclimate', - 'accompany', - 'account', - 'accuracy', - 'accurate', - 'accustom', - 'acetone', - 'achiness', - 'aching', - 'acid', - 'acorn', - 'acquaint', - 'acquire', - 'acre', - 'acrobat', - 'acronym', - 'acting', - 'action', - 'activate', - 'activator', - 'active', - 'activism', - 'activist', - 'activity', - 'actress', - 'acts', - 'acutely', - 'acuteness', - 'aeration', - 'aerobics', - 'aerosol', - 'aerospace', - 'afar', - 'affair', - 'affected', - 'affecting', - 'affection', - 'affidavit', - 'affiliate', - 'affirm', - 'affix', - 'afflicted', - 'affluent', - 'afford', - 'affront', - 'aflame', - 'afloat', - 'aflutter', - 'afoot', - 'afraid', - 'afterglow', - 'afterlife', - 'aftermath', - 'aftermost', - 'afternoon', - 'aged', - 'ageless', - 'agency', - 'agenda', - 'agent', - 'aggregate', - 'aghast', - 'agile', - 'agility', - 'aging', - 'agnostic', - 'agonize', - 'agonizing', - 'agony', - 'agreeable', - 'agreeably', - 'agreed', - 'agreeing', - 'agreement', - 'aground', - 'ahead', - 'ahoy', - 'aide', - 'aids', - 'aim', - 'ajar', - 'alabaster', - 'alarm', - 'albatross', - 'album', - 'alfalfa', - 'algebra', - 'algorithm', - 'alias', - 'alibi', - 'alienable', - 'alienate', - 'aliens', - 'alike', - 'alive', - 'alkaline', - 'alkalize', - 'almanac', - 'almighty', - 'almost', - 'aloe', - 'aloft', - 'aloha', - 'alone', - 'alongside', - 'aloof', - 'alphabet', - 'alright', - 'although', - 'altitude', - 'alto', - 'aluminum', - 'alumni', - 'always', - 'amaretto', - 'amaze', - 'amazingly', - 'amber', - 'ambiance', - 'ambiguity', - 'ambiguous', - 'ambition', - 'ambitious', - 'ambulance', - 'ambush', - 'amendable', - 'amendment', - 'amends', - 'amenity', - 'amiable', - 'amicably', - 'amid', - 'amigo', - 'amino', - 'amiss', - 'ammonia', - 'ammonium', - 'amnesty', - 'amniotic', - 'among', - 'amount', - 'amperage', - 'ample', - 'amplifier', - 'amplify', - 'amply', - 'amuck', - 'amulet', - 'amusable', - 'amused', - 'amusement', - 'amuser', - 'amusing', - 'anaconda', - 'anaerobic', - 'anagram', - 'anatomist', - 'anatomy', - 'anchor', - 'anchovy', - 'ancient', - 'android', - 'anemia', - 'anemic', - 'aneurism', - 'anew', - 'angelfish', - 'angelic', - 'anger', - 'angled', - 'angler', - 'angles', - 'angling', - 'angrily', - 'angriness', - 'anguished', - 'angular', - 'animal', - 'animate', - 'animating', - 'animation', - 'animator', - 'anime', - 'animosity', - 'ankle', - 'annex', - 'annotate', - 'announcer', - 'annoying', - 'annually', - 'annuity', - 'anointer', - 'another', - 'answering', - 'antacid', - 'antarctic', - 'anteater', - 'antelope', - 'antennae', - 'anthem', - 'anthill', - 'anthology', - 'antibody', - 'antics', - 'antidote', - 'antihero', - 'antiquely', - 'antiques', - 'antiquity', - 'antirust', - 'antitoxic', - 'antitrust', - 'antiviral', - 'antivirus', - 'antler', - 'antonym', - 'antsy', - 'anvil', - 'anybody', - 'anyhow', - 'anymore', - 'anyone', - 'anyplace', - 'anything', - 'anytime', - 'anyway', - 'anywhere', - 'aorta', - 'apache', - 'apostle', - 'appealing', - 'appear', - 'appease', - 'appeasing', - 'appendage', - 'appendix', - 'appetite', - 'appetizer', - 'applaud', - 'applause', - 'apple', - 'appliance', - 'applicant', - 'applied', - 'apply', - 'appointee', - 'appraisal', - 'appraiser', - 'apprehend', - 'approach', - 'approval', - 'approve', - 'apricot', - 'april', - 'apron', - 'aptitude', - 'aptly', - 'aqua', - 'aqueduct', - 'arbitrary', - 'arbitrate', - 'ardently', - 'area', - 'arena', - 'arguable', - 'arguably', - 'argue', - 'arise', - 'armadillo', - 'armband', - 'armchair', - 'armed', - 'armful', - 'armhole', - 'arming', - 'armless', - 'armoire', - 'armored', - 'armory', - 'armrest', - 'army', - 'aroma', - 'arose', - 'around', - 'arousal', - 'arrange', - 'array', - 'arrest', - 'arrival', - 'arrive', - 'arrogance', - 'arrogant', - 'arson', - 'art', - 'ascend', - 'ascension', - 'ascent', - 'ascertain', - 'ashamed', - 'ashen', - 'ashes', - 'ashy', - 'aside', - 'askew', - 'asleep', - 'asparagus', - 'aspect', - 'aspirate', - 'aspire', - 'aspirin', - 'astonish', - 'astound', - 'astride', - 'astrology', - 'astronaut', - 'astronomy', - 'astute', - 'atlantic', - 'atlas', - 'atom', - 'atonable', - 'atop', - 'atrium', - 'atrocious', - 'atrophy', - 'attach', - 'attain', - 'attempt', - 'attendant', - 'attendee', - 'attention', - 'attentive', - 'attest', - 'attic', - 'attire', - 'attitude', - 'attractor', - 'attribute', - 'atypical', - 'auction', - 'audacious', - 'audacity', - 'audible', - 'audibly', - 'audience', - 'audio', - 'audition', - 'augmented', - 'august', - 'authentic', - 'author', - 'autism', - 'autistic', - 'autograph', - 'automaker', - 'automated', - 'automatic', - 'autopilot', - 'available', - 'avalanche', - 'avatar', - 'avenge', - 'avenging', - 'avenue', - 'average', - 'aversion', - 'avert', - 'aviation', - 'aviator', - 'avid', - 'avoid', - 'await', - 'awaken', - 'award', - 'aware', - 'awhile', - 'awkward', - 'awning', - 'awoke', - 'awry', - 'axis', - 'babble', - 'babbling', - 'babied', - 'baboon', - 'backache', - 'backboard', - 'backboned', - 'backdrop', - 'backed', - 'backer', - 'backfield', - 'backfire', - 'backhand', - 'backing', - 'backlands', - 'backlash', - 'backless', - 'backlight', - 'backlit', - 'backlog', - 'backpack', - 'backpedal', - 'backrest', - 'backroom', - 'backshift', - 'backside', - 'backslid', - 'backspace', - 'backspin', - 'backstab', - 'backstage', - 'backtalk', - 'backtrack', - 'backup', - 'backward', - 'backwash', - 'backwater', - 'backyard', - 'bacon', - 'bacteria', - 'bacterium', - 'badass', - 'badge', - 'badland', - 'badly', - 'badness', - 'baffle', - 'baffling', - 'bagel', - 'bagful', - 'baggage', - 'bagged', - 'baggie', - 'bagginess', - 'bagging', - 'baggy', - 'bagpipe', - 'baguette', - 'baked', - 'bakery', - 'bakeshop', - 'baking', - 'balance', - 'balancing', - 'balcony', - 'balmy', - 'balsamic', - 'bamboo', - 'banana', - 'banish', - 'banister', - 'banjo', - 'bankable', - 'bankbook', - 'banked', - 'banker', - 'banking', - 'banknote', - 'bankroll', - 'banner', - 'bannister', - 'banshee', - 'banter', - 'barbecue', - 'barbed', - 'barbell', - 'barber', - 'barcode', - 'barge', - 'bargraph', - 'barista', - 'baritone', - 'barley', - 'barmaid', - 'barman', - 'barn', - 'barometer', - 'barrack', - 'barracuda', - 'barrel', - 'barrette', - 'barricade', - 'barrier', - 'barstool', - 'bartender', - 'barterer', - 'bash', - 'basically', - 'basics', - 'basil', - 'basin', - 'basis', - 'basket', - 'batboy', - 'batch', - 'bath', - 'baton', - 'bats', - 'battalion', - 'battered', - 'battering', - 'battery', - 'batting', - 'battle', - 'bauble', - 'bazooka', - 'blabber', - 'bladder', - 'blade', - 'blah', - 'blame', - 'blaming', - 'blanching', - 'blandness', - 'blank', - 'blaspheme', - 'blasphemy', - 'blast', - 'blatancy', - 'blatantly', - 'blazer', - 'blazing', - 'bleach', - 'bleak', - 'bleep', - 'blemish', - 'blend', - 'bless', - 'blighted', - 'blimp', - 'bling', - 'blinked', - 'blinker', - 'blinking', - 'blinks', - 'blip', - 'blissful', - 'blitz', - 'blizzard', - 'bloated', - 'bloating', - 'blob', - 'blog', - 'bloomers', - 'blooming', - 'blooper', - 'blot', - 'blouse', - 'blubber', - 'bluff', - 'bluish', - 'blunderer', - 'blunt', - 'blurb', - 'blurred', - 'blurry', - 'blurt', - 'blush', - 'blustery', - 'boaster', - 'boastful', - 'boasting', - 'boat', - 'bobbed', - 'bobbing', - 'bobble', - 'bobcat', - 'bobsled', - 'bobtail', - 'bodacious', - 'body', - 'bogged', - 'boggle', - 'bogus', - 'boil', - 'bok', - 'bolster', - 'bolt', - 'bonanza', - 'bonded', - 'bonding', - 'bondless', - 'boned', - 'bonehead', - 'boneless', - 'bonelike', - 'boney', - 'bonfire', - 'bonnet', - 'bonsai', - 'bonus', - 'bony', - 'boogeyman', - 'boogieman', - 'book', - 'boondocks', - 'booted', - 'booth', - 'bootie', - 'booting', - 'bootlace', - 'bootleg', - 'boots', - 'boozy', - 'borax', - 'boring', - 'borough', - 'borrower', - 'borrowing', - 'boss', - 'botanical', - 'botanist', - 'botany', - 'botch', - 'both', - 'bottle', - 'bottling', - 'bottom', - 'bounce', - 'bouncing', - 'bouncy', - 'bounding', - 'boundless', - 'bountiful', - 'bovine', - 'boxcar', - 'boxer', - 'boxing', - 'boxlike', - 'boxy', - 'breach', - 'breath', - 'breeches', - 'breeching', - 'breeder', - 'breeding', - 'breeze', - 'breezy', - 'brethren', - 'brewery', - 'brewing', - 'briar', - 'bribe', - 'brick', - 'bride', - 'bridged', - 'brigade', - 'bright', - 'brilliant', - 'brim', - 'bring', - 'brink', - 'brisket', - 'briskly', - 'briskness', - 'bristle', - 'brittle', - 'broadband', - 'broadcast', - 'broaden', - 'broadly', - 'broadness', - 'broadside', - 'broadways', - 'broiler', - 'broiling', - 'broken', - 'broker', - 'bronchial', - 'bronco', - 'bronze', - 'bronzing', - 'brook', - 'broom', - 'brought', - 'browbeat', - 'brownnose', - 'browse', - 'browsing', - 'bruising', - 'brunch', - 'brunette', - 'brunt', - 'brush', - 'brussels', - 'brute', - 'brutishly', - 'bubble', - 'bubbling', - 'bubbly', - 'buccaneer', - 'bucked', - 'bucket', - 'buckle', - 'buckshot', - 'buckskin', - 'bucktooth', - 'buckwheat', - 'buddhism', - 'buddhist', - 'budding', - 'buddy', - 'budget', - 'buffalo', - 'buffed', - 'buffer', - 'buffing', - 'buffoon', - 'buggy', - 'bulb', - 'bulge', - 'bulginess', - 'bulgur', - 'bulk', - 'bulldog', - 'bulldozer', - 'bullfight', - 'bullfrog', - 'bullhorn', - 'bullion', - 'bullish', - 'bullpen', - 'bullring', - 'bullseye', - 'bullwhip', - 'bully', - 'bunch', - 'bundle', - 'bungee', - 'bunion', - 'bunkbed', - 'bunkhouse', - 'bunkmate', - 'bunny', - 'bunt', - 'busboy', - 'bush', - 'busily', - 'busload', - 'bust', - 'busybody', - 'buzz', - 'cabana', - 'cabbage', - 'cabbie', - 'cabdriver', - 'cable', - 'caboose', - 'cache', - 'cackle', - 'cacti', - 'cactus', - 'caddie', - 'caddy', - 'cadet', - 'cadillac', - 'cadmium', - 'cage', - 'cahoots', - 'cake', - 'calamari', - 'calamity', - 'calcium', - 'calculate', - 'calculus', - 'caliber', - 'calibrate', - 'calm', - 'caloric', - 'calorie', - 'calzone', - 'camcorder', - 'cameo', - 'camera', - 'camisole', - 'camper', - 'campfire', - 'camping', - 'campsite', - 'campus', - 'canal', - 'canary', - 'cancel', - 'candied', - 'candle', - 'candy', - 'cane', - 'canine', - 'canister', - 'cannabis', - 'canned', - 'canning', - 'cannon', - 'cannot', - 'canola', - 'canon', - 'canopener', - 'canopy', - 'canteen', - 'canyon', - 'capable', - 'capably', - 'capacity', - 'cape', - 'capillary', - 'capital', - 'capitol', - 'capped', - 'capricorn', - 'capsize', - 'capsule', - 'caption', - 'captivate', - 'captive', - 'captivity', - 'capture', - 'caramel', - 'carat', - 'caravan', - 'carbon', - 'cardboard', - 'carded', - 'cardiac', - 'cardigan', - 'cardinal', - 'cardstock', - 'carefully', - 'caregiver', - 'careless', - 'caress', - 'caretaker', - 'cargo', - 'caring', - 'carless', - 'carload', - 'carmaker', - 'carnage', - 'carnation', - 'carnival', - 'carnivore', - 'carol', - 'carpenter', - 'carpentry', - 'carpool', - 'carport', - 'carried', - 'carrot', - 'carrousel', - 'carry', - 'cartel', - 'cartload', - 'carton', - 'cartoon', - 'cartridge', - 'cartwheel', - 'carve', - 'carving', - 'carwash', - 'cascade', - 'case', - 'cash', - 'casing', - 'casino', - 'casket', - 'cassette', - 'casually', - 'casualty', - 'catacomb', - 'catalog', - 'catalyst', - 'catalyze', - 'catapult', - 'cataract', - 'catatonic', - 'catcall', - 'catchable', - 'catcher', - 'catching', - 'catchy', - 'caterer', - 'catering', - 'catfight', - 'catfish', - 'cathedral', - 'cathouse', - 'catlike', - 'catnap', - 'catnip', - 'catsup', - 'cattail', - 'cattishly', - 'cattle', - 'catty', - 'catwalk', - 'caucasian', - 'caucus', - 'causal', - 'causation', - 'cause', - 'causing', - 'cauterize', - 'caution', - 'cautious', - 'cavalier', - 'cavalry', - 'caviar', - 'cavity', - 'cedar', - 'celery', - 'celestial', - 'celibacy', - 'celibate', - 'celtic', - 'cement', - 'census', - 'ceramics', - 'ceremony', - 'certainly', - 'certainty', - 'certified', - 'certify', - 'cesarean', - 'cesspool', - 'chafe', - 'chaffing', - 'chain', - 'chair', - 'chalice', - 'challenge', - 'chamber', - 'chamomile', - 'champion', - 'chance', - 'change', - 'channel', - 'chant', - 'chaos', - 'chaperone', - 'chaplain', - 'chapped', - 'chaps', - 'chapter', - 'character', - 'charbroil', - 'charcoal', - 'charger', - 'charging', - 'chariot', - 'charity', - 'charm', - 'charred', - 'charter', - 'charting', - 'chase', - 'chasing', - 'chaste', - 'chastise', - 'chastity', - 'chatroom', - 'chatter', - 'chatting', - 'chatty', - 'cheating', - 'cheddar', - 'cheek', - 'cheer', - 'cheese', - 'cheesy', - 'chef', - 'chemicals', - 'chemist', - 'chemo', - 'cherisher', - 'cherub', - 'chess', - 'chest', - 'chevron', - 'chevy', - 'chewable', - 'chewer', - 'chewing', - 'chewy', - 'chief', - 'chihuahua', - 'childcare', - 'childhood', - 'childish', - 'childless', - 'childlike', - 'chili', - 'chill', - 'chimp', - 'chip', - 'chirping', - 'chirpy', - 'chitchat', - 'chivalry', - 'chive', - 'chloride', - 'chlorine', - 'choice', - 'chokehold', - 'choking', - 'chomp', - 'chooser', - 'choosing', - 'choosy', - 'chop', - 'chosen', - 'chowder', - 'chowtime', - 'chrome', - 'chubby', - 'chuck', - 'chug', - 'chummy', - 'chump', - 'chunk', - 'churn', - 'chute', - 'cider', - 'cilantro', - 'cinch', - 'cinema', - 'cinnamon', - 'circle', - 'circling', - 'circular', - 'circulate', - 'circus', - 'citable', - 'citadel', - 'citation', - 'citizen', - 'citric', - 'citrus', - 'city', - 'civic', - 'civil', - 'clad', - 'claim', - 'clambake', - 'clammy', - 'clamor', - 'clamp', - 'clamshell', - 'clang', - 'clanking', - 'clapped', - 'clapper', - 'clapping', - 'clarify', - 'clarinet', - 'clarity', - 'clash', - 'clasp', - 'class', - 'clatter', - 'clause', - 'clavicle', - 'claw', - 'clay', - 'clean', - 'clear', - 'cleat', - 'cleaver', - 'cleft', - 'clench', - 'clergyman', - 'clerical', - 'clerk', - 'clever', - 'clicker', - 'client', - 'climate', - 'climatic', - 'cling', - 'clinic', - 'clinking', - 'clip', - 'clique', - 'cloak', - 'clobber', - 'clock', - 'clone', - 'cloning', - 'closable', - 'closure', - 'clothes', - 'clothing', - 'cloud', - 'clover', - 'clubbed', - 'clubbing', - 'clubhouse', - 'clump', - 'clumsily', - 'clumsy', - 'clunky', - 'clustered', - 'clutch', - 'clutter', - 'coach', - 'coagulant', - 'coastal', - 'coaster', - 'coasting', - 'coastland', - 'coastline', - 'coat', - 'coauthor', - 'cobalt', - 'cobbler', - 'cobweb', - 'cocoa', - 'coconut', - 'cod', - 'coeditor', - 'coerce', - 'coexist', - 'coffee', - 'cofounder', - 'cognition', - 'cognitive', - 'cogwheel', - 'coherence', - 'coherent', - 'cohesive', - 'coil', - 'coke', - 'cola', - 'cold', - 'coleslaw', - 'coliseum', - 'collage', - 'collapse', - 'collar', - 'collected', - 'collector', - 'collide', - 'collie', - 'collision', - 'colonial', - 'colonist', - 'colonize', - 'colony', - 'colossal', - 'colt', - 'coma', - 'come', - 'comfort', - 'comfy', - 'comic', - 'coming', - 'comma', - 'commence', - 'commend', - 'comment', - 'commerce', - 'commode', - 'commodity', - 'commodore', - 'common', - 'commotion', - 'commute', - 'commuting', - 'compacted', - 'compacter', - 'compactly', - 'compactor', - 'companion', - 'company', - 'compare', - 'compel', - 'compile', - 'comply', - 'component', - 'composed', - 'composer', - 'composite', - 'compost', - 'composure', - 'compound', - 'compress', - 'comprised', - 'computer', - 'computing', - 'comrade', - 'concave', - 'conceal', - 'conceded', - 'concept', - 'concerned', - 'concert', - 'conch', - 'concierge', - 'concise', - 'conclude', - 'concrete', - 'concur', - 'condense', - 'condiment', - 'condition', - 'condone', - 'conducive', - 'conductor', - 'conduit', - 'cone', - 'confess', - 'confetti', - 'confidant', - 'confident', - 'confider', - 'confiding', - 'configure', - 'confined', - 'confining', - 'confirm', - 'conflict', - 'conform', - 'confound', - 'confront', - 'confused', - 'confusing', - 'confusion', - 'congenial', - 'congested', - 'congrats', - 'congress', - 'conical', - 'conjoined', - 'conjure', - 'conjuror', - 'connected', - 'connector', - 'consensus', - 'consent', - 'console', - 'consoling', - 'consonant', - 'constable', - 'constant', - 'constrain', - 'constrict', - 'construct', - 'consult', - 'consumer', - 'consuming', - 'contact', - 'container', - 'contempt', - 'contend', - 'contented', - 'contently', - 'contents', - 'contest', - 'context', - 'contort', - 'contour', - 'contrite', - 'control', - 'contusion', - 'convene', - 'convent', - 'copartner', - 'cope', - 'copied', - 'copier', - 'copilot', - 'coping', - 'copious', - 'copper', - 'copy', - 'coral', - 'cork', - 'cornball', - 'cornbread', - 'corncob', - 'cornea', - 'corned', - 'corner', - 'cornfield', - 'cornflake', - 'cornhusk', - 'cornmeal', - 'cornstalk', - 'corny', - 'coronary', - 'coroner', - 'corporal', - 'corporate', - 'corral', - 'correct', - 'corridor', - 'corrode', - 'corroding', - 'corrosive', - 'corsage', - 'corset', - 'cortex', - 'cosigner', - 'cosmetics', - 'cosmic', - 'cosmos', - 'cosponsor', - 'cost', - 'cottage', - 'cotton', - 'couch', - 'cough', - 'could', - 'countable', - 'countdown', - 'counting', - 'countless', - 'country', - 'county', - 'courier', - 'covenant', - 'cover', - 'coveted', - 'coveting', - 'coyness', - 'cozily', - 'coziness', - 'cozy', - 'crabbing', - 'crabgrass', - 'crablike', - 'crabmeat', - 'cradle', - 'cradling', - 'crafter', - 'craftily', - 'craftsman', - 'craftwork', - 'crafty', - 'cramp', - 'cranberry', - 'crane', - 'cranial', - 'cranium', - 'crank', - 'crate', - 'crave', - 'craving', - 'crawfish', - 'crawlers', - 'crawling', - 'crayfish', - 'crayon', - 'crazed', - 'crazily', - 'craziness', - 'crazy', - 'creamed', - 'creamer', - 'creamlike', - 'crease', - 'creasing', - 'creatable', - 'create', - 'creation', - 'creative', - 'creature', - 'credible', - 'credibly', - 'credit', - 'creed', - 'creme', - 'creole', - 'crepe', - 'crept', - 'crescent', - 'crested', - 'cresting', - 'crestless', - 'crevice', - 'crewless', - 'crewman', - 'crewmate', - 'crib', - 'cricket', - 'cried', - 'crier', - 'crimp', - 'crimson', - 'cringe', - 'cringing', - 'crinkle', - 'crinkly', - 'crisped', - 'crisping', - 'crisply', - 'crispness', - 'crispy', - 'criteria', - 'critter', - 'croak', - 'crock', - 'crook', - 'croon', - 'crop', - 'cross', - 'crouch', - 'crouton', - 'crowbar', - 'crowd', - 'crown', - 'crucial', - 'crudely', - 'crudeness', - 'cruelly', - 'cruelness', - 'cruelty', - 'crumb', - 'crummiest', - 'crummy', - 'crumpet', - 'crumpled', - 'cruncher', - 'crunching', - 'crunchy', - 'crusader', - 'crushable', - 'crushed', - 'crusher', - 'crushing', - 'crust', - 'crux', - 'crying', - 'cryptic', - 'crystal', - 'cubbyhole', - 'cube', - 'cubical', - 'cubicle', - 'cucumber', - 'cuddle', - 'cuddly', - 'cufflink', - 'culinary', - 'culminate', - 'culpable', - 'culprit', - 'cultivate', - 'cultural', - 'culture', - 'cupbearer', - 'cupcake', - 'cupid', - 'cupped', - 'cupping', - 'curable', - 'curator', - 'curdle', - 'cure', - 'curfew', - 'curing', - 'curled', - 'curler', - 'curliness', - 'curling', - 'curly', - 'curry', - 'curse', - 'cursive', - 'cursor', - 'curtain', - 'curtly', - 'curtsy', - 'curvature', - 'curve', - 'curvy', - 'cushy', - 'cusp', - 'cussed', - 'custard', - 'custodian', - 'custody', - 'customary', - 'customer', - 'customize', - 'customs', - 'cut', - 'cycle', - 'cyclic', - 'cycling', - 'cyclist', - 'cylinder', - 'cymbal', - 'cytoplasm', - 'cytoplast', - 'dab', - 'dad', - 'daffodil', - 'dagger', - 'daily', - 'daintily', - 'dainty', - 'dairy', - 'daisy', - 'dallying', - 'dance', - 'dancing', - 'dandelion', - 'dander', - 'dandruff', - 'dandy', - 'danger', - 'dangle', - 'dangling', - 'daredevil', - 'dares', - 'daringly', - 'darkened', - 'darkening', - 'darkish', - 'darkness', - 'darkroom', - 'darling', - 'darn', - 'dart', - 'darwinism', - 'dash', - 'dastardly', - 'data', - 'datebook', - 'dating', - 'daughter', - 'daunting', - 'dawdler', - 'dawn', - 'daybed', - 'daybreak', - 'daycare', - 'daydream', - 'daylight', - 'daylong', - 'dayroom', - 'daytime', - 'dazzler', - 'dazzling', - 'deacon', - 'deafening', - 'deafness', - 'dealer', - 'dealing', - 'dealmaker', - 'dealt', - 'dean', - 'debatable', - 'debate', - 'debating', - 'debit', - 'debrief', - 'debtless', - 'debtor', - 'debug', - 'debunk', - 'decade', - 'decaf', - 'decal', - 'decathlon', - 'decay', - 'deceased', - 'deceit', - 'deceiver', - 'deceiving', - 'december', - 'decency', - 'decent', - 'deception', - 'deceptive', - 'decibel', - 'decidable', - 'decimal', - 'decimeter', - 'decipher', - 'deck', - 'declared', - 'decline', - 'decode', - 'decompose', - 'decorated', - 'decorator', - 'decoy', - 'decrease', - 'decree', - 'dedicate', - 'dedicator', - 'deduce', - 'deduct', - 'deed', - 'deem', - 'deepen', - 'deeply', - 'deepness', - 'deface', - 'defacing', - 'defame', - 'default', - 'defeat', - 'defection', - 'defective', - 'defendant', - 'defender', - 'defense', - 'defensive', - 'deferral', - 'deferred', - 'defiance', - 'defiant', - 'defile', - 'defiling', - 'define', - 'definite', - 'deflate', - 'deflation', - 'deflator', - 'deflected', - 'deflector', - 'defog', - 'deforest', - 'defraud', - 'defrost', - 'deftly', - 'defuse', - 'defy', - 'degraded', - 'degrading', - 'degrease', - 'degree', - 'dehydrate', - 'deity', - 'dejected', - 'delay', - 'delegate', - 'delegator', - 'delete', - 'deletion', - 'delicacy', - 'delicate', - 'delicious', - 'delighted', - 'delirious', - 'delirium', - 'deliverer', - 'delivery', - 'delouse', - 'delta', - 'deluge', - 'delusion', - 'deluxe', - 'demanding', - 'demeaning', - 'demeanor', - 'demise', - 'democracy', - 'democrat', - 'demote', - 'demotion', - 'demystify', - 'denatured', - 'deniable', - 'denial', - 'denim', - 'denote', - 'dense', - 'density', - 'dental', - 'dentist', - 'denture', - 'deny', - 'deodorant', - 'deodorize', - 'departed', - 'departure', - 'depict', - 'deplete', - 'depletion', - 'deplored', - 'deploy', - 'deport', - 'depose', - 'depraved', - 'depravity', - 'deprecate', - 'depress', - 'deprive', - 'depth', - 'deputize', - 'deputy', - 'derail', - 'deranged', - 'derby', - 'derived', - 'desecrate', - 'deserve', - 'deserving', - 'designate', - 'designed', - 'designer', - 'designing', - 'deskbound', - 'desktop', - 'deskwork', - 'desolate', - 'despair', - 'despise', - 'despite', - 'destiny', - 'destitute', - 'destruct', - 'detached', - 'detail', - 'detection', - 'detective', - 'detector', - 'detention', - 'detergent', - 'detest', - 'detonate', - 'detonator', - 'detoxify', - 'detract', - 'deuce', - 'devalue', - 'deviancy', - 'deviant', - 'deviate', - 'deviation', - 'deviator', - 'device', - 'devious', - 'devotedly', - 'devotee', - 'devotion', - 'devourer', - 'devouring', - 'devoutly', - 'dexterity', - 'dexterous', - 'diabetes', - 'diabetic', - 'diabolic', - 'diagnoses', - 'diagnosis', - 'diagram', - 'dial', - 'diameter', - 'diaper', - 'diaphragm', - 'diary', - 'dice', - 'dicing', - 'dictate', - 'dictation', - 'dictator', - 'difficult', - 'diffused', - 'diffuser', - 'diffusion', - 'diffusive', - 'dig', - 'dilation', - 'diligence', - 'diligent', - 'dill', - 'dilute', - 'dime', - 'diminish', - 'dimly', - 'dimmed', - 'dimmer', - 'dimness', - 'dimple', - 'diner', - 'dingbat', - 'dinghy', - 'dinginess', - 'dingo', - 'dingy', - 'dining', - 'dinner', - 'diocese', - 'dioxide', - 'diploma', - 'dipped', - 'dipper', - 'dipping', - 'directed', - 'direction', - 'directive', - 'directly', - 'directory', - 'direness', - 'dirtiness', - 'disabled', - 'disagree', - 'disallow', - 'disarm', - 'disarray', - 'disaster', - 'disband', - 'disbelief', - 'disburse', - 'discard', - 'discern', - 'discharge', - 'disclose', - 'discolor', - 'discount', - 'discourse', - 'discover', - 'discuss', - 'disdain', - 'disengage', - 'disfigure', - 'disgrace', - 'dish', - 'disinfect', - 'disjoin', - 'disk', - 'dislike', - 'disliking', - 'dislocate', - 'dislodge', - 'disloyal', - 'dismantle', - 'dismay', - 'dismiss', - 'dismount', - 'disobey', - 'disorder', - 'disown', - 'disparate', - 'disparity', - 'dispatch', - 'dispense', - 'dispersal', - 'dispersed', - 'disperser', - 'displace', - 'display', - 'displease', - 'disposal', - 'dispose', - 'disprove', - 'dispute', - 'disregard', - 'disrupt', - 'dissuade', - 'distance', - 'distant', - 'distaste', - 'distill', - 'distinct', - 'distort', - 'distract', - 'distress', - 'district', - 'distrust', - 'ditch', - 'ditto', - 'ditzy', - 'dividable', - 'divided', - 'dividend', - 'dividers', - 'dividing', - 'divinely', - 'diving', - 'divinity', - 'divisible', - 'divisibly', - 'division', - 'divisive', - 'divorcee', - 'dizziness', - 'dizzy', - 'doable', - 'docile', - 'dock', - 'doctrine', - 'document', - 'dodge', - 'dodgy', - 'doily', - 'doing', - 'dole', - 'dollar', - 'dollhouse', - 'dollop', - 'dolly', - 'dolphin', - 'domain', - 'domelike', - 'domestic', - 'dominion', - 'dominoes', - 'donated', - 'donation', - 'donator', - 'donor', - 'donut', - 'doodle', - 'doorbell', - 'doorframe', - 'doorknob', - 'doorman', - 'doormat', - 'doornail', - 'doorpost', - 'doorstep', - 'doorstop', - 'doorway', - 'doozy', - 'dork', - 'dormitory', - 'dorsal', - 'dosage', - 'dose', - 'dotted', - 'doubling', - 'douche', - 'dove', - 'down', - 'dowry', - 'doze', - 'drab', - 'dragging', - 'dragonfly', - 'dragonish', - 'dragster', - 'drainable', - 'drainage', - 'drained', - 'drainer', - 'drainpipe', - 'dramatic', - 'dramatize', - 'drank', - 'drapery', - 'drastic', - 'draw', - 'dreaded', - 'dreadful', - 'dreadlock', - 'dreamboat', - 'dreamily', - 'dreamland', - 'dreamless', - 'dreamlike', - 'dreamt', - 'dreamy', - 'drearily', - 'dreary', - 'drench', - 'dress', - 'drew', - 'dribble', - 'dried', - 'drier', - 'drift', - 'driller', - 'drilling', - 'drinkable', - 'drinking', - 'dripping', - 'drippy', - 'drivable', - 'driven', - 'driver', - 'driveway', - 'driving', - 'drizzle', - 'drizzly', - 'drone', - 'drool', - 'droop', - 'drop-down', - 'dropbox', - 'dropkick', - 'droplet', - 'dropout', - 'dropper', - 'drove', - 'drown', - 'drowsily', - 'drudge', - 'drum', - 'dry', - 'dubbed', - 'dubiously', - 'duchess', - 'duckbill', - 'ducking', - 'duckling', - 'ducktail', - 'ducky', - 'duct', - 'dude', - 'duffel', - 'dugout', - 'duh', - 'duke', - 'duller', - 'dullness', - 'duly', - 'dumping', - 'dumpling', - 'dumpster', - 'duo', - 'dupe', - 'duplex', - 'duplicate', - 'duplicity', - 'durable', - 'durably', - 'duration', - 'duress', - 'during', - 'dusk', - 'dust', - 'dutiful', - 'duty', - 'duvet', - 'dwarf', - 'dweeb', - 'dwelled', - 'dweller', - 'dwelling', - 'dwindle', - 'dwindling', - 'dynamic', - 'dynamite', - 'dynasty', - 'dyslexia', - 'dyslexic', - 'each', - 'eagle', - 'earache', - 'eardrum', - 'earflap', - 'earful', - 'earlobe', - 'early', - 'earmark', - 'earmuff', - 'earphone', - 'earpiece', - 'earplugs', - 'earring', - 'earshot', - 'earthen', - 'earthlike', - 'earthling', - 'earthly', - 'earthworm', - 'earthy', - 'earwig', - 'easeful', - 'easel', - 'easiest', - 'easily', - 'easiness', - 'easing', - 'eastbound', - 'eastcoast', - 'easter', - 'eastward', - 'eatable', - 'eaten', - 'eatery', - 'eating', - 'eats', - 'ebay', - 'ebony', - 'ebook', - 'ecard', - 'eccentric', - 'echo', - 'eclair', - 'eclipse', - 'ecologist', - 'ecology', - 'economic', - 'economist', - 'economy', - 'ecosphere', - 'ecosystem', - 'edge', - 'edginess', - 'edging', - 'edgy', - 'edition', - 'editor', - 'educated', - 'education', - 'educator', - 'eel', - 'effective', - 'effects', - 'efficient', - 'effort', - 'eggbeater', - 'egging', - 'eggnog', - 'eggplant', - 'eggshell', - 'egomaniac', - 'egotism', - 'egotistic', - 'either', - 'eject', - 'elaborate', - 'elastic', - 'elated', - 'elbow', - 'eldercare', - 'elderly', - 'eldest', - 'electable', - 'election', - 'elective', - 'elephant', - 'elevate', - 'elevating', - 'elevation', - 'elevator', - 'eleven', - 'elf', - 'eligible', - 'eligibly', - 'eliminate', - 'elite', - 'elitism', - 'elixir', - 'elk', - 'ellipse', - 'elliptic', - 'elm', - 'elongated', - 'elope', - 'eloquence', - 'eloquent', - 'elsewhere', - 'elude', - 'elusive', - 'elves', - 'email', - 'embargo', - 'embark', - 'embassy', - 'embattled', - 'embellish', - 'ember', - 'embezzle', - 'emblaze', - 'emblem', - 'embody', - 'embolism', - 'emboss', - 'embroider', - 'emcee', - 'emerald', - 'emergency', - 'emission', - 'emit', - 'emote', - 'emoticon', - 'emotion', - 'empathic', - 'empathy', - 'emperor', - 'emphases', - 'emphasis', - 'emphasize', - 'emphatic', - 'empirical', - 'employed', - 'employee', - 'employer', - 'emporium', - 'empower', - 'emptier', - 'emptiness', - 'empty', - 'emu', - 'enable', - 'enactment', - 'enamel', - 'enchanted', - 'enchilada', - 'encircle', - 'enclose', - 'enclosure', - 'encode', - 'encore', - 'encounter', - 'encourage', - 'encroach', - 'encrust', - 'encrypt', - 'endanger', - 'endeared', - 'endearing', - 'ended', - 'ending', - 'endless', - 'endnote', - 'endocrine', - 'endorphin', - 'endorse', - 'endowment', - 'endpoint', - 'endurable', - 'endurance', - 'enduring', - 'energetic', - 'energize', - 'energy', - 'enforced', - 'enforcer', - 'engaged', - 'engaging', - 'engine', - 'engorge', - 'engraved', - 'engraver', - 'engraving', - 'engross', - 'engulf', - 'enhance', - 'enigmatic', - 'enjoyable', - 'enjoyably', - 'enjoyer', - 'enjoying', - 'enjoyment', - 'enlarged', - 'enlarging', - 'enlighten', - 'enlisted', - 'enquirer', - 'enrage', - 'enrich', - 'enroll', - 'enslave', - 'ensnare', - 'ensure', - 'entail', - 'entangled', - 'entering', - 'entertain', - 'enticing', - 'entire', - 'entitle', - 'entity', - 'entomb', - 'entourage', - 'entrap', - 'entree', - 'entrench', - 'entrust', - 'entryway', - 'entwine', - 'enunciate', - 'envelope', - 'enviable', - 'enviably', - 'envious', - 'envision', - 'envoy', - 'envy', - 'enzyme', - 'epic', - 'epidemic', - 'epidermal', - 'epidermis', - 'epidural', - 'epilepsy', - 'epileptic', - 'epilogue', - 'epiphany', - 'episode', - 'equal', - 'equate', - 'equation', - 'equator', - 'equinox', - 'equipment', - 'equity', - 'equivocal', - 'eradicate', - 'erasable', - 'erased', - 'eraser', - 'erasure', - 'ergonomic', - 'errand', - 'errant', - 'erratic', - 'error', - 'erupt', - 'escalate', - 'escalator', - 'escapable', - 'escapade', - 'escapist', - 'escargot', - 'eskimo', - 'esophagus', - 'espionage', - 'espresso', - 'esquire', - 'essay', - 'essence', - 'essential', - 'establish', - 'estate', - 'esteemed', - 'estimate', - 'estimator', - 'estranged', - 'estrogen', - 'etching', - 'eternal', - 'eternity', - 'ethanol', - 'ether', - 'ethically', - 'ethics', - 'euphemism', - 'evacuate', - 'evacuee', - 'evade', - 'evaluate', - 'evaluator', - 'evaporate', - 'evasion', - 'evasive', - 'even', - 'everglade', - 'evergreen', - 'everybody', - 'everyday', - 'everyone', - 'evict', - 'evidence', - 'evident', - 'evil', - 'evoke', - 'evolution', - 'evolve', - 'exact', - 'exalted', - 'example', - 'excavate', - 'excavator', - 'exceeding', - 'exception', - 'excess', - 'exchange', - 'excitable', - 'exciting', - 'exclaim', - 'exclude', - 'excluding', - 'exclusion', - 'exclusive', - 'excretion', - 'excretory', - 'excursion', - 'excusable', - 'excusably', - 'excuse', - 'exemplary', - 'exemplify', - 'exemption', - 'exerciser', - 'exert', - 'exes', - 'exfoliate', - 'exhale', - 'exhaust', - 'exhume', - 'exile', - 'existing', - 'exit', - 'exodus', - 'exonerate', - 'exorcism', - 'exorcist', - 'expand', - 'expanse', - 'expansion', - 'expansive', - 'expectant', - 'expedited', - 'expediter', - 'expel', - 'expend', - 'expenses', - 'expensive', - 'expert', - 'expire', - 'expiring', - 'explain', - 'expletive', - 'explicit', - 'explode', - 'exploit', - 'explore', - 'exploring', - 'exponent', - 'exporter', - 'exposable', - 'expose', - 'exposure', - 'express', - 'expulsion', - 'exquisite', - 'extended', - 'extending', - 'extent', - 'extenuate', - 'exterior', - 'external', - 'extinct', - 'extortion', - 'extradite', - 'extras', - 'extrovert', - 'extrude', - 'extruding', - 'exuberant', - 'fable', - 'fabric', - 'fabulous', - 'facebook', - 'facecloth', - 'facedown', - 'faceless', - 'facelift', - 'faceplate', - 'faceted', - 'facial', - 'facility', - 'facing', - 'facsimile', - 'faction', - 'factoid', - 'factor', - 'factsheet', - 'factual', - 'faculty', - 'fade', - 'fading', - 'failing', - 'falcon', - 'fall', - 'false', - 'falsify', - 'fame', - 'familiar', - 'family', - 'famine', - 'famished', - 'fanatic', - 'fancied', - 'fanciness', - 'fancy', - 'fanfare', - 'fang', - 'fanning', - 'fantasize', - 'fantastic', - 'fantasy', - 'fascism', - 'fastball', - 'faster', - 'fasting', - 'fastness', - 'faucet', - 'favorable', - 'favorably', - 'favored', - 'favoring', - 'favorite', - 'fax', - 'feast', - 'federal', - 'fedora', - 'feeble', - 'feed', - 'feel', - 'feisty', - 'feline', - 'felt-tip', - 'feminine', - 'feminism', - 'feminist', - 'feminize', - 'femur', - 'fence', - 'fencing', - 'fender', - 'ferment', - 'fernlike', - 'ferocious', - 'ferocity', - 'ferret', - 'ferris', - 'ferry', - 'fervor', - 'fester', - 'festival', - 'festive', - 'festivity', - 'fetal', - 'fetch', - 'fever', - 'fiber', - 'fiction', - 'fiddle', - 'fiddling', - 'fidelity', - 'fidgeting', - 'fidgety', - 'fifteen', - 'fifth', - 'fiftieth', - 'fifty', - 'figment', - 'figure', - 'figurine', - 'filing', - 'filled', - 'filler', - 'filling', - 'film', - 'filter', - 'filth', - 'filtrate', - 'finale', - 'finalist', - 'finalize', - 'finally', - 'finance', - 'financial', - 'finch', - 'fineness', - 'finer', - 'finicky', - 'finished', - 'finisher', - 'finishing', - 'finite', - 'finless', - 'finlike', - 'fiscally', - 'fit', - 'five', - 'flaccid', - 'flagman', - 'flagpole', - 'flagship', - 'flagstick', - 'flagstone', - 'flail', - 'flakily', - 'flaky', - 'flame', - 'flammable', - 'flanked', - 'flanking', - 'flannels', - 'flap', - 'flaring', - 'flashback', - 'flashbulb', - 'flashcard', - 'flashily', - 'flashing', - 'flashy', - 'flask', - 'flatbed', - 'flatfoot', - 'flatly', - 'flatness', - 'flatten', - 'flattered', - 'flatterer', - 'flattery', - 'flattop', - 'flatware', - 'flatworm', - 'flavored', - 'flavorful', - 'flavoring', - 'flaxseed', - 'fled', - 'fleshed', - 'fleshy', - 'flick', - 'flier', - 'flight', - 'flinch', - 'fling', - 'flint', - 'flip', - 'flirt', - 'float', - 'flock', - 'flogging', - 'flop', - 'floral', - 'florist', - 'floss', - 'flounder', - 'flyable', - 'flyaway', - 'flyer', - 'flying', - 'flyover', - 'flypaper', - 'foam', - 'foe', - 'fog', - 'foil', - 'folic', - 'folk', - 'follicle', - 'follow', - 'fondling', - 'fondly', - 'fondness', - 'fondue', - 'font', - 'food', - 'fool', - 'footage', - 'football', - 'footbath', - 'footboard', - 'footer', - 'footgear', - 'foothill', - 'foothold', - 'footing', - 'footless', - 'footman', - 'footnote', - 'footpad', - 'footpath', - 'footprint', - 'footrest', - 'footsie', - 'footsore', - 'footwear', - 'footwork', - 'fossil', - 'foster', - 'founder', - 'founding', - 'fountain', - 'fox', - 'foyer', - 'fraction', - 'fracture', - 'fragile', - 'fragility', - 'fragment', - 'fragrance', - 'fragrant', - 'frail', - 'frame', - 'framing', - 'frantic', - 'fraternal', - 'frayed', - 'fraying', - 'frays', - 'freckled', - 'freckles', - 'freebase', - 'freebee', - 'freebie', - 'freedom', - 'freefall', - 'freehand', - 'freeing', - 'freeload', - 'freely', - 'freemason', - 'freeness', - 'freestyle', - 'freeware', - 'freeway', - 'freewill', - 'freezable', - 'freezing', - 'freight', - 'french', - 'frenzied', - 'frenzy', - 'frequency', - 'frequent', - 'fresh', - 'fretful', - 'fretted', - 'friction', - 'friday', - 'fridge', - 'fried', - 'friend', - 'frighten', - 'frightful', - 'frigidity', - 'frigidly', - 'frill', - 'fringe', - 'frisbee', - 'frisk', - 'fritter', - 'frivolous', - 'frolic', - 'from', - 'front', - 'frostbite', - 'frosted', - 'frostily', - 'frosting', - 'frostlike', - 'frosty', - 'froth', - 'frown', - 'frozen', - 'fructose', - 'frugality', - 'frugally', - 'fruit', - 'frustrate', - 'frying', - 'gab', - 'gaffe', - 'gag', - 'gainfully', - 'gaining', - 'gains', - 'gala', - 'gallantly', - 'galleria', - 'gallery', - 'galley', - 'gallon', - 'gallows', - 'gallstone', - 'galore', - 'galvanize', - 'gambling', - 'game', - 'gaming', - 'gamma', - 'gander', - 'gangly', - 'gangrene', - 'gangway', - 'gap', - 'garage', - 'garbage', - 'garden', - 'gargle', - 'garland', - 'garlic', - 'garment', - 'garnet', - 'garnish', - 'garter', - 'gas', - 'gatherer', - 'gathering', - 'gating', - 'gauging', - 'gauntlet', - 'gauze', - 'gave', - 'gawk', - 'gazing', - 'gear', - 'gecko', - 'geek', - 'geiger', - 'gem', - 'gender', - 'generic', - 'generous', - 'genetics', - 'genre', - 'gentile', - 'gentleman', - 'gently', - 'gents', - 'geography', - 'geologic', - 'geologist', - 'geology', - 'geometric', - 'geometry', - 'geranium', - 'gerbil', - 'geriatric', - 'germicide', - 'germinate', - 'germless', - 'germproof', - 'gestate', - 'gestation', - 'gesture', - 'getaway', - 'getting', - 'getup', - 'giant', - 'gibberish', - 'giblet', - 'giddily', - 'giddiness', - 'giddy', - 'gift', - 'gigabyte', - 'gigahertz', - 'gigantic', - 'giggle', - 'giggling', - 'giggly', - 'gigolo', - 'gilled', - 'gills', - 'gimmick', - 'girdle', - 'giveaway', - 'given', - 'giver', - 'giving', - 'gizmo', - 'gizzard', - 'glacial', - 'glacier', - 'glade', - 'gladiator', - 'gladly', - 'glamorous', - 'glamour', - 'glance', - 'glancing', - 'glandular', - 'glare', - 'glaring', - 'glass', - 'glaucoma', - 'glazing', - 'gleaming', - 'gleeful', - 'glider', - 'gliding', - 'glimmer', - 'glimpse', - 'glisten', - 'glitch', - 'glitter', - 'glitzy', - 'gloater', - 'gloating', - 'gloomily', - 'gloomy', - 'glorified', - 'glorifier', - 'glorify', - 'glorious', - 'glory', - 'gloss', - 'glove', - 'glowing', - 'glowworm', - 'glucose', - 'glue', - 'gluten', - 'glutinous', - 'glutton', - 'gnarly', - 'gnat', - 'goal', - 'goatskin', - 'goes', - 'goggles', - 'going', - 'goldfish', - 'goldmine', - 'goldsmith', - 'golf', - 'goliath', - 'gonad', - 'gondola', - 'gone', - 'gong', - 'good', - 'gooey', - 'goofball', - 'goofiness', - 'goofy', - 'google', - 'goon', - 'gopher', - 'gore', - 'gorged', - 'gorgeous', - 'gory', - 'gosling', - 'gossip', - 'gothic', - 'gotten', - 'gout', - 'gown', - 'grab', - 'graceful', - 'graceless', - 'gracious', - 'gradation', - 'graded', - 'grader', - 'gradient', - 'grading', - 'gradually', - 'graduate', - 'graffiti', - 'grafted', - 'grafting', - 'grain', - 'granddad', - 'grandkid', - 'grandly', - 'grandma', - 'grandpa', - 'grandson', - 'granite', - 'granny', - 'granola', - 'grant', - 'granular', - 'grape', - 'graph', - 'grapple', - 'grappling', - 'grasp', - 'grass', - 'gratified', - 'gratify', - 'grating', - 'gratitude', - 'gratuity', - 'gravel', - 'graveness', - 'graves', - 'graveyard', - 'gravitate', - 'gravity', - 'gravy', - 'gray', - 'grazing', - 'greasily', - 'greedily', - 'greedless', - 'greedy', - 'green', - 'greeter', - 'greeting', - 'grew', - 'greyhound', - 'grid', - 'grief', - 'grievance', - 'grieving', - 'grievous', - 'grill', - 'grimace', - 'grimacing', - 'grime', - 'griminess', - 'grimy', - 'grinch', - 'grinning', - 'grip', - 'gristle', - 'grit', - 'groggily', - 'groggy', - 'groin', - 'groom', - 'groove', - 'grooving', - 'groovy', - 'grope', - 'ground', - 'grouped', - 'grout', - 'grove', - 'grower', - 'growing', - 'growl', - 'grub', - 'grudge', - 'grudging', - 'grueling', - 'gruffly', - 'grumble', - 'grumbling', - 'grumbly', - 'grumpily', - 'grunge', - 'grunt', - 'guacamole', - 'guidable', - 'guidance', - 'guide', - 'guiding', - 'guileless', - 'guise', - 'gulf', - 'gullible', - 'gully', - 'gulp', - 'gumball', - 'gumdrop', - 'gumminess', - 'gumming', - 'gummy', - 'gurgle', - 'gurgling', - 'guru', - 'gush', - 'gusto', - 'gusty', - 'gutless', - 'guts', - 'gutter', - 'guy', - 'guzzler', - 'gyration', - 'habitable', - 'habitant', - 'habitat', - 'habitual', - 'hacked', - 'hacker', - 'hacking', - 'hacksaw', - 'had', - 'haggler', - 'haiku', - 'half', - 'halogen', - 'halt', - 'halved', - 'halves', - 'hamburger', - 'hamlet', - 'hammock', - 'hamper', - 'hamster', - 'hamstring', - 'handbag', - 'handball', - 'handbook', - 'handbrake', - 'handcart', - 'handclap', - 'handclasp', - 'handcraft', - 'handcuff', - 'handed', - 'handful', - 'handgrip', - 'handgun', - 'handheld', - 'handiness', - 'handiwork', - 'handlebar', - 'handled', - 'handler', - 'handling', - 'handmade', - 'handoff', - 'handpick', - 'handprint', - 'handrail', - 'handsaw', - 'handset', - 'handsfree', - 'handshake', - 'handstand', - 'handwash', - 'handwork', - 'handwoven', - 'handwrite', - 'handyman', - 'hangnail', - 'hangout', - 'hangover', - 'hangup', - 'hankering', - 'hankie', - 'hanky', - 'haphazard', - 'happening', - 'happier', - 'happiest', - 'happily', - 'happiness', - 'happy', - 'harbor', - 'hardcopy', - 'hardcore', - 'hardcover', - 'harddisk', - 'hardened', - 'hardener', - 'hardening', - 'hardhat', - 'hardhead', - 'hardiness', - 'hardly', - 'hardness', - 'hardship', - 'hardware', - 'hardwired', - 'hardwood', - 'hardy', - 'harmful', - 'harmless', - 'harmonica', - 'harmonics', - 'harmonize', - 'harmony', - 'harness', - 'harpist', - 'harsh', - 'harvest', - 'hash', - 'hassle', - 'haste', - 'hastily', - 'hastiness', - 'hasty', - 'hatbox', - 'hatchback', - 'hatchery', - 'hatchet', - 'hatching', - 'hatchling', - 'hate', - 'hatless', - 'hatred', - 'haunt', - 'haven', - 'hazard', - 'hazelnut', - 'hazily', - 'haziness', - 'hazing', - 'hazy', - 'headache', - 'headband', - 'headboard', - 'headcount', - 'headdress', - 'headed', - 'header', - 'headfirst', - 'headgear', - 'heading', - 'headlamp', - 'headless', - 'headlock', - 'headphone', - 'headpiece', - 'headrest', - 'headroom', - 'headscarf', - 'headset', - 'headsman', - 'headstand', - 'headstone', - 'headway', - 'headwear', - 'heap', - 'heat', - 'heave', - 'heavily', - 'heaviness', - 'heaving', - 'hedge', - 'hedging', - 'heftiness', - 'hefty', - 'helium', - 'helmet', - 'helper', - 'helpful', - 'helping', - 'helpless', - 'helpline', - 'hemlock', - 'hemstitch', - 'hence', - 'henchman', - 'henna', - 'herald', - 'herbal', - 'herbicide', - 'herbs', - 'heritage', - 'hermit', - 'heroics', - 'heroism', - 'herring', - 'herself', - 'hertz', - 'hesitancy', - 'hesitant', - 'hesitate', - 'hexagon', - 'hexagram', - 'hubcap', - 'huddle', - 'huddling', - 'huff', - 'hug', - 'hula', - 'hulk', - 'hull', - 'human', - 'humble', - 'humbling', - 'humbly', - 'humid', - 'humiliate', - 'humility', - 'humming', - 'hummus', - 'humongous', - 'humorist', - 'humorless', - 'humorous', - 'humpback', - 'humped', - 'humvee', - 'hunchback', - 'hundredth', - 'hunger', - 'hungrily', - 'hungry', - 'hunk', - 'hunter', - 'hunting', - 'huntress', - 'huntsman', - 'hurdle', - 'hurled', - 'hurler', - 'hurling', - 'hurray', - 'hurricane', - 'hurried', - 'hurry', - 'hurt', - 'husband', - 'hush', - 'husked', - 'huskiness', - 'hut', - 'hybrid', - 'hydrant', - 'hydrated', - 'hydration', - 'hydrogen', - 'hydroxide', - 'hyperlink', - 'hypertext', - 'hyphen', - 'hypnoses', - 'hypnosis', - 'hypnotic', - 'hypnotism', - 'hypnotist', - 'hypnotize', - 'hypocrisy', - 'hypocrite', - 'ibuprofen', - 'ice', - 'iciness', - 'icing', - 'icky', - 'icon', - 'icy', - 'idealism', - 'idealist', - 'idealize', - 'ideally', - 'idealness', - 'identical', - 'identify', - 'identity', - 'ideology', - 'idiocy', - 'idiom', - 'idly', - 'igloo', - 'ignition', - 'ignore', - 'iguana', - 'illicitly', - 'illusion', - 'illusive', - 'image', - 'imaginary', - 'imagines', - 'imaging', - 'imbecile', - 'imitate', - 'imitation', - 'immature', - 'immerse', - 'immersion', - 'imminent', - 'immobile', - 'immodest', - 'immorally', - 'immortal', - 'immovable', - 'immovably', - 'immunity', - 'immunize', - 'impaired', - 'impale', - 'impart', - 'impatient', - 'impeach', - 'impeding', - 'impending', - 'imperfect', - 'imperial', - 'impish', - 'implant', - 'implement', - 'implicate', - 'implicit', - 'implode', - 'implosion', - 'implosive', - 'imply', - 'impolite', - 'important', - 'importer', - 'impose', - 'imposing', - 'impotence', - 'impotency', - 'impotent', - 'impound', - 'imprecise', - 'imprint', - 'imprison', - 'impromptu', - 'improper', - 'improve', - 'improving', - 'improvise', - 'imprudent', - 'impulse', - 'impulsive', - 'impure', - 'impurity', - 'iodine', - 'iodize', - 'ion', - 'ipad', - 'iphone', - 'ipod', - 'irate', - 'irk', - 'iron', - 'irregular', - 'irrigate', - 'irritable', - 'irritably', - 'irritant', - 'irritate', - 'islamic', - 'islamist', - 'isolated', - 'isolating', - 'isolation', - 'isotope', - 'issue', - 'issuing', - 'italicize', - 'italics', - 'item', - 'itinerary', - 'itunes', - 'ivory', - 'ivy', - 'jab', - 'jackal', - 'jacket', - 'jackknife', - 'jackpot', - 'jailbird', - 'jailbreak', - 'jailer', - 'jailhouse', - 'jalapeno', - 'jam', - 'janitor', - 'january', - 'jargon', - 'jarring', - 'jasmine', - 'jaundice', - 'jaunt', - 'java', - 'jawed', - 'jawless', - 'jawline', - 'jaws', - 'jaybird', - 'jaywalker', - 'jazz', - 'jeep', - 'jeeringly', - 'jellied', - 'jelly', - 'jersey', - 'jester', - 'jet', - 'jiffy', - 'jigsaw', - 'jimmy', - 'jingle', - 'jingling', - 'jinx', - 'jitters', - 'jittery', - 'job', - 'jockey', - 'jockstrap', - 'jogger', - 'jogging', - 'john', - 'joining', - 'jokester', - 'jokingly', - 'jolliness', - 'jolly', - 'jolt', - 'jot', - 'jovial', - 'joyfully', - 'joylessly', - 'joyous', - 'joyride', - 'joystick', - 'jubilance', - 'jubilant', - 'judge', - 'judgingly', - 'judicial', - 'judiciary', - 'judo', - 'juggle', - 'juggling', - 'jugular', - 'juice', - 'juiciness', - 'juicy', - 'jujitsu', - 'jukebox', - 'july', - 'jumble', - 'jumbo', - 'jump', - 'junction', - 'juncture', - 'june', - 'junior', - 'juniper', - 'junkie', - 'junkman', - 'junkyard', - 'jurist', - 'juror', - 'jury', - 'justice', - 'justifier', - 'justify', - 'justly', - 'justness', - 'juvenile', - 'kabob', - 'kangaroo', - 'karaoke', - 'karate', - 'karma', - 'kebab', - 'keenly', - 'keenness', - 'keep', - 'keg', - 'kelp', - 'kennel', - 'kept', - 'kerchief', - 'kerosene', - 'kettle', - 'kick', - 'kiln', - 'kilobyte', - 'kilogram', - 'kilometer', - 'kilowatt', - 'kilt', - 'kimono', - 'kindle', - 'kindling', - 'kindly', - 'kindness', - 'kindred', - 'kinetic', - 'kinfolk', - 'king', - 'kinship', - 'kinsman', - 'kinswoman', - 'kissable', - 'kisser', - 'kissing', - 'kitchen', - 'kite', - 'kitten', - 'kitty', - 'kiwi', - 'kleenex', - 'knapsack', - 'knee', - 'knelt', - 'knickers', - 'knoll', - 'koala', - 'kooky', - 'kosher', - 'krypton', - 'kudos', - 'kung', - 'labored', - 'laborer', - 'laboring', - 'laborious', - 'labrador', - 'ladder', - 'ladies', - 'ladle', - 'ladybug', - 'ladylike', - 'lagged', - 'lagging', - 'lagoon', - 'lair', - 'lake', - 'lance', - 'landed', - 'landfall', - 'landfill', - 'landing', - 'landlady', - 'landless', - 'landline', - 'landlord', - 'landmark', - 'landmass', - 'landmine', - 'landowner', - 'landscape', - 'landside', - 'landslide', - 'language', - 'lankiness', - 'lanky', - 'lantern', - 'lapdog', - 'lapel', - 'lapped', - 'lapping', - 'laptop', - 'lard', - 'large', - 'lark', - 'lash', - 'lasso', - 'last', - 'latch', - 'late', - 'lather', - 'latitude', - 'latrine', - 'latter', - 'latticed', - 'launch', - 'launder', - 'laundry', - 'laurel', - 'lavender', - 'lavish', - 'laxative', - 'lazily', - 'laziness', - 'lazy', - 'lecturer', - 'left', - 'legacy', - 'legal', - 'legend', - 'legged', - 'leggings', - 'legible', - 'legibly', - 'legislate', - 'lego', - 'legroom', - 'legume', - 'legwarmer', - 'legwork', - 'lemon', - 'lend', - 'length', - 'lens', - 'lent', - 'leotard', - 'lesser', - 'letdown', - 'lethargic', - 'lethargy', - 'letter', - 'lettuce', - 'level', - 'leverage', - 'levers', - 'levitate', - 'levitator', - 'liability', - 'liable', - 'liberty', - 'librarian', - 'library', - 'licking', - 'licorice', - 'lid', - 'life', - 'lifter', - 'lifting', - 'liftoff', - 'ligament', - 'likely', - 'likeness', - 'likewise', - 'liking', - 'lilac', - 'lilly', - 'lily', - 'limb', - 'limeade', - 'limelight', - 'limes', - 'limit', - 'limping', - 'limpness', - 'line', - 'lingo', - 'linguini', - 'linguist', - 'lining', - 'linked', - 'linoleum', - 'linseed', - 'lint', - 'lion', - 'lip', - 'liquefy', - 'liqueur', - 'liquid', - 'lisp', - 'list', - 'litigate', - 'litigator', - 'litmus', - 'litter', - 'little', - 'livable', - 'lived', - 'lively', - 'liver', - 'livestock', - 'lividly', - 'living', - 'lizard', - 'lubricant', - 'lubricate', - 'lucid', - 'luckily', - 'luckiness', - 'luckless', - 'lucrative', - 'ludicrous', - 'lugged', - 'lukewarm', - 'lullaby', - 'lumber', - 'luminance', - 'luminous', - 'lumpiness', - 'lumping', - 'lumpish', - 'lunacy', - 'lunar', - 'lunchbox', - 'luncheon', - 'lunchroom', - 'lunchtime', - 'lung', - 'lurch', - 'lure', - 'luridness', - 'lurk', - 'lushly', - 'lushness', - 'luster', - 'lustfully', - 'lustily', - 'lustiness', - 'lustrous', - 'lusty', - 'luxurious', - 'luxury', - 'lying', - 'lyrically', - 'lyricism', - 'lyricist', - 'lyrics', - 'macarena', - 'macaroni', - 'macaw', - 'mace', - 'machine', - 'machinist', - 'magazine', - 'magenta', - 'maggot', - 'magical', - 'magician', - 'magma', - 'magnesium', - 'magnetic', - 'magnetism', - 'magnetize', - 'magnifier', - 'magnify', - 'magnitude', - 'magnolia', - 'mahogany', - 'maimed', - 'majestic', - 'majesty', - 'majorette', - 'majority', - 'makeover', - 'maker', - 'makeshift', - 'making', - 'malformed', - 'malt', - 'mama', - 'mammal', - 'mammary', - 'mammogram', - 'manager', - 'managing', - 'manatee', - 'mandarin', - 'mandate', - 'mandatory', - 'mandolin', - 'manger', - 'mangle', - 'mango', - 'mangy', - 'manhandle', - 'manhole', - 'manhood', - 'manhunt', - 'manicotti', - 'manicure', - 'manifesto', - 'manila', - 'mankind', - 'manlike', - 'manliness', - 'manly', - 'manmade', - 'manned', - 'mannish', - 'manor', - 'manpower', - 'mantis', - 'mantra', - 'manual', - 'many', - 'map', - 'marathon', - 'marauding', - 'marbled', - 'marbles', - 'marbling', - 'march', - 'mardi', - 'margarine', - 'margarita', - 'margin', - 'marigold', - 'marina', - 'marine', - 'marital', - 'maritime', - 'marlin', - 'marmalade', - 'maroon', - 'married', - 'marrow', - 'marry', - 'marshland', - 'marshy', - 'marsupial', - 'marvelous', - 'marxism', - 'mascot', - 'masculine', - 'mashed', - 'mashing', - 'massager', - 'masses', - 'massive', - 'mastiff', - 'matador', - 'matchbook', - 'matchbox', - 'matcher', - 'matching', - 'matchless', - 'material', - 'maternal', - 'maternity', - 'math', - 'mating', - 'matriarch', - 'matrimony', - 'matrix', - 'matron', - 'matted', - 'matter', - 'maturely', - 'maturing', - 'maturity', - 'mauve', - 'maverick', - 'maximize', - 'maximum', - 'maybe', - 'mayday', - 'mayflower', - 'moaner', - 'moaning', - 'mobile', - 'mobility', - 'mobilize', - 'mobster', - 'mocha', - 'mocker', - 'mockup', - 'modified', - 'modify', - 'modular', - 'modulator', - 'module', - 'moisten', - 'moistness', - 'moisture', - 'molar', - 'molasses', - 'mold', - 'molecular', - 'molecule', - 'molehill', - 'mollusk', - 'mom', - 'monastery', - 'monday', - 'monetary', - 'monetize', - 'moneybags', - 'moneyless', - 'moneywise', - 'mongoose', - 'mongrel', - 'monitor', - 'monkhood', - 'monogamy', - 'monogram', - 'monologue', - 'monopoly', - 'monorail', - 'monotone', - 'monotype', - 'monoxide', - 'monsieur', - 'monsoon', - 'monstrous', - 'monthly', - 'monument', - 'moocher', - 'moodiness', - 'moody', - 'mooing', - 'moonbeam', - 'mooned', - 'moonlight', - 'moonlike', - 'moonlit', - 'moonrise', - 'moonscape', - 'moonshine', - 'moonstone', - 'moonwalk', - 'mop', - 'morale', - 'morality', - 'morally', - 'morbidity', - 'morbidly', - 'morphine', - 'morphing', - 'morse', - 'mortality', - 'mortally', - 'mortician', - 'mortified', - 'mortify', - 'mortuary', - 'mosaic', - 'mossy', - 'most', - 'mothball', - 'mothproof', - 'motion', - 'motivate', - 'motivator', - 'motive', - 'motocross', - 'motor', - 'motto', - 'mountable', - 'mountain', - 'mounted', - 'mounting', - 'mourner', - 'mournful', - 'mouse', - 'mousiness', - 'moustache', - 'mousy', - 'mouth', - 'movable', - 'move', - 'movie', - 'moving', - 'mower', - 'mowing', - 'much', - 'muck', - 'mud', - 'mug', - 'mulberry', - 'mulch', - 'mule', - 'mulled', - 'mullets', - 'multiple', - 'multiply', - 'multitask', - 'multitude', - 'mumble', - 'mumbling', - 'mumbo', - 'mummified', - 'mummify', - 'mummy', - 'mumps', - 'munchkin', - 'mundane', - 'municipal', - 'muppet', - 'mural', - 'murkiness', - 'murky', - 'murmuring', - 'muscular', - 'museum', - 'mushily', - 'mushiness', - 'mushroom', - 'mushy', - 'music', - 'musket', - 'muskiness', - 'musky', - 'mustang', - 'mustard', - 'muster', - 'mustiness', - 'musty', - 'mutable', - 'mutate', - 'mutation', - 'mute', - 'mutilated', - 'mutilator', - 'mutiny', - 'mutt', - 'mutual', - 'muzzle', - 'myself', - 'myspace', - 'mystified', - 'mystify', - 'myth', - 'nacho', - 'nag', - 'nail', - 'name', - 'naming', - 'nanny', - 'nanometer', - 'nape', - 'napkin', - 'napped', - 'napping', - 'nappy', - 'narrow', - 'nastily', - 'nastiness', - 'national', - 'native', - 'nativity', - 'natural', - 'nature', - 'naturist', - 'nautical', - 'navigate', - 'navigator', - 'navy', - 'nearby', - 'nearest', - 'nearly', - 'nearness', - 'neatly', - 'neatness', - 'nebula', - 'nebulizer', - 'nectar', - 'negate', - 'negation', - 'negative', - 'neglector', - 'negligee', - 'negligent', - 'negotiate', - 'nemeses', - 'nemesis', - 'neon', - 'nephew', - 'nerd', - 'nervous', - 'nervy', - 'nest', - 'net', - 'neurology', - 'neuron', - 'neurosis', - 'neurotic', - 'neuter', - 'neutron', - 'never', - 'next', - 'nibble', - 'nickname', - 'nicotine', - 'niece', - 'nifty', - 'nimble', - 'nimbly', - 'nineteen', - 'ninetieth', - 'ninja', - 'nintendo', - 'ninth', - 'nuclear', - 'nuclei', - 'nucleus', - 'nugget', - 'nullify', - 'number', - 'numbing', - 'numbly', - 'numbness', - 'numeral', - 'numerate', - 'numerator', - 'numeric', - 'numerous', - 'nuptials', - 'nursery', - 'nursing', - 'nurture', - 'nutcase', - 'nutlike', - 'nutmeg', - 'nutrient', - 'nutshell', - 'nuttiness', - 'nutty', - 'nuzzle', - 'nylon', - 'oaf', - 'oak', - 'oasis', - 'oat', - 'obedience', - 'obedient', - 'obituary', - 'object', - 'obligate', - 'obliged', - 'oblivion', - 'oblivious', - 'oblong', - 'obnoxious', - 'oboe', - 'obscure', - 'obscurity', - 'observant', - 'observer', - 'observing', - 'obsessed', - 'obsession', - 'obsessive', - 'obsolete', - 'obstacle', - 'obstinate', - 'obstruct', - 'obtain', - 'obtrusive', - 'obtuse', - 'obvious', - 'occultist', - 'occupancy', - 'occupant', - 'occupier', - 'occupy', - 'ocean', - 'ocelot', - 'octagon', - 'octane', - 'october', - 'octopus', - 'ogle', - 'oil', - 'oink', - 'ointment', - 'okay', - 'old', - 'olive', - 'olympics', - 'omega', - 'omen', - 'ominous', - 'omission', - 'omit', - 'omnivore', - 'onboard', - 'oncoming', - 'ongoing', - 'onion', - 'online', - 'onlooker', - 'only', - 'onscreen', - 'onset', - 'onshore', - 'onslaught', - 'onstage', - 'onto', - 'onward', - 'onyx', - 'oops', - 'ooze', - 'oozy', - 'opacity', - 'opal', - 'open', - 'operable', - 'operate', - 'operating', - 'operation', - 'operative', - 'operator', - 'opium', - 'opossum', - 'opponent', - 'oppose', - 'opposing', - 'opposite', - 'oppressed', - 'oppressor', - 'opt', - 'opulently', - 'osmosis', - 'other', - 'otter', - 'ouch', - 'ought', - 'ounce', - 'outage', - 'outback', - 'outbid', - 'outboard', - 'outbound', - 'outbreak', - 'outburst', - 'outcast', - 'outclass', - 'outcome', - 'outdated', - 'outdoors', - 'outer', - 'outfield', - 'outfit', - 'outflank', - 'outgoing', - 'outgrow', - 'outhouse', - 'outing', - 'outlast', - 'outlet', - 'outline', - 'outlook', - 'outlying', - 'outmatch', - 'outmost', - 'outnumber', - 'outplayed', - 'outpost', - 'outpour', - 'output', - 'outrage', - 'outrank', - 'outreach', - 'outright', - 'outscore', - 'outsell', - 'outshine', - 'outshoot', - 'outsider', - 'outskirts', - 'outsmart', - 'outsource', - 'outspoken', - 'outtakes', - 'outthink', - 'outward', - 'outweigh', - 'outwit', - 'oval', - 'ovary', - 'oven', - 'overact', - 'overall', - 'overarch', - 'overbid', - 'overbill', - 'overbite', - 'overblown', - 'overboard', - 'overbook', - 'overbuilt', - 'overcast', - 'overcoat', - 'overcome', - 'overcook', - 'overcrowd', - 'overdraft', - 'overdrawn', - 'overdress', - 'overdrive', - 'overdue', - 'overeager', - 'overeater', - 'overexert', - 'overfed', - 'overfeed', - 'overfill', - 'overflow', - 'overfull', - 'overgrown', - 'overhand', - 'overhang', - 'overhaul', - 'overhead', - 'overhear', - 'overheat', - 'overhung', - 'overjoyed', - 'overkill', - 'overlabor', - 'overlaid', - 'overlap', - 'overlay', - 'overload', - 'overlook', - 'overlord', - 'overlying', - 'overnight', - 'overpass', - 'overpay', - 'overplant', - 'overplay', - 'overpower', - 'overprice', - 'overrate', - 'overreach', - 'overreact', - 'override', - 'overripe', - 'overrule', - 'overrun', - 'overshoot', - 'overshot', - 'oversight', - 'oversized', - 'oversleep', - 'oversold', - 'overspend', - 'overstate', - 'overstay', - 'overstep', - 'overstock', - 'overstuff', - 'oversweet', - 'overtake', - 'overthrow', - 'overtime', - 'overtly', - 'overtone', - 'overture', - 'overturn', - 'overuse', - 'overvalue', - 'overview', - 'overwrite', - 'owl', - 'oxford', - 'oxidant', - 'oxidation', - 'oxidize', - 'oxidizing', - 'oxygen', - 'oxymoron', - 'oyster', - 'ozone', - 'paced', - 'pacemaker', - 'pacific', - 'pacifier', - 'pacifism', - 'pacifist', - 'pacify', - 'padded', - 'padding', - 'paddle', - 'paddling', - 'padlock', - 'pagan', - 'pager', - 'paging', - 'pajamas', - 'palace', - 'palatable', - 'palm', - 'palpable', - 'palpitate', - 'paltry', - 'pampered', - 'pamperer', - 'pampers', - 'pamphlet', - 'panama', - 'pancake', - 'pancreas', - 'panda', - 'pandemic', - 'pang', - 'panhandle', - 'panic', - 'panning', - 'panorama', - 'panoramic', - 'panther', - 'pantomime', - 'pantry', - 'pants', - 'pantyhose', - 'paparazzi', - 'papaya', - 'paper', - 'paprika', - 'papyrus', - 'parabola', - 'parachute', - 'parade', - 'paradox', - 'paragraph', - 'parakeet', - 'paralegal', - 'paralyses', - 'paralysis', - 'paralyze', - 'paramedic', - 'parameter', - 'paramount', - 'parasail', - 'parasite', - 'parasitic', - 'parcel', - 'parched', - 'parchment', - 'pardon', - 'parish', - 'parka', - 'parking', - 'parkway', - 'parlor', - 'parmesan', - 'parole', - 'parrot', - 'parsley', - 'parsnip', - 'partake', - 'parted', - 'parting', - 'partition', - 'partly', - 'partner', - 'partridge', - 'party', - 'passable', - 'passably', - 'passage', - 'passcode', - 'passenger', - 'passerby', - 'passing', - 'passion', - 'passive', - 'passivism', - 'passover', - 'passport', - 'password', - 'pasta', - 'pasted', - 'pastel', - 'pastime', - 'pastor', - 'pastrami', - 'pasture', - 'pasty', - 'patchwork', - 'patchy', - 'paternal', - 'paternity', - 'path', - 'patience', - 'patient', - 'patio', - 'patriarch', - 'patriot', - 'patrol', - 'patronage', - 'patronize', - 'pauper', - 'pavement', - 'paver', - 'pavestone', - 'pavilion', - 'paving', - 'pawing', - 'payable', - 'payback', - 'paycheck', - 'payday', - 'payee', - 'payer', - 'paying', - 'payment', - 'payphone', - 'payroll', - 'pebble', - 'pebbly', - 'pecan', - 'pectin', - 'peculiar', - 'peddling', - 'pediatric', - 'pedicure', - 'pedigree', - 'pedometer', - 'pegboard', - 'pelican', - 'pellet', - 'pelt', - 'pelvis', - 'penalize', - 'penalty', - 'pencil', - 'pendant', - 'pending', - 'penholder', - 'penknife', - 'pennant', - 'penniless', - 'penny', - 'penpal', - 'pension', - 'pentagon', - 'pentagram', - 'pep', - 'perceive', - 'percent', - 'perch', - 'percolate', - 'perennial', - 'perfected', - 'perfectly', - 'perfume', - 'periscope', - 'perish', - 'perjurer', - 'perjury', - 'perkiness', - 'perky', - 'perm', - 'peroxide', - 'perpetual', - 'perplexed', - 'persecute', - 'persevere', - 'persuaded', - 'persuader', - 'pesky', - 'peso', - 'pessimism', - 'pessimist', - 'pester', - 'pesticide', - 'petal', - 'petite', - 'petition', - 'petri', - 'petroleum', - 'petted', - 'petticoat', - 'pettiness', - 'petty', - 'petunia', - 'phantom', - 'phobia', - 'phoenix', - 'phonebook', - 'phoney', - 'phonics', - 'phoniness', - 'phony', - 'phosphate', - 'photo', - 'phrase', - 'phrasing', - 'placard', - 'placate', - 'placidly', - 'plank', - 'planner', - 'plant', - 'plasma', - 'plaster', - 'plastic', - 'plated', - 'platform', - 'plating', - 'platinum', - 'platonic', - 'platter', - 'platypus', - 'plausible', - 'plausibly', - 'playable', - 'playback', - 'player', - 'playful', - 'playgroup', - 'playhouse', - 'playing', - 'playlist', - 'playmaker', - 'playmate', - 'playoff', - 'playpen', - 'playroom', - 'playset', - 'plaything', - 'playtime', - 'plaza', - 'pleading', - 'pleat', - 'pledge', - 'plentiful', - 'plenty', - 'plethora', - 'plexiglas', - 'pliable', - 'plod', - 'plop', - 'plot', - 'plow', - 'ploy', - 'pluck', - 'plug', - 'plunder', - 'plunging', - 'plural', - 'plus', - 'plutonium', - 'plywood', - 'poach', - 'pod', - 'poem', - 'poet', - 'pogo', - 'pointed', - 'pointer', - 'pointing', - 'pointless', - 'pointy', - 'poise', - 'poison', - 'poker', - 'poking', - 'polar', - 'police', - 'policy', - 'polio', - 'polish', - 'politely', - 'polka', - 'polo', - 'polyester', - 'polygon', - 'polygraph', - 'polymer', - 'poncho', - 'pond', - 'pony', - 'popcorn', - 'pope', - 'poplar', - 'popper', - 'poppy', - 'popsicle', - 'populace', - 'popular', - 'populate', - 'porcupine', - 'pork', - 'porous', - 'porridge', - 'portable', - 'portal', - 'portfolio', - 'porthole', - 'portion', - 'portly', - 'portside', - 'poser', - 'posh', - 'posing', - 'possible', - 'possibly', - 'possum', - 'postage', - 'postal', - 'postbox', - 'postcard', - 'posted', - 'poster', - 'posting', - 'postnasal', - 'posture', - 'postwar', - 'pouch', - 'pounce', - 'pouncing', - 'pound', - 'pouring', - 'pout', - 'powdered', - 'powdering', - 'powdery', - 'power', - 'powwow', - 'pox', - 'praising', - 'prance', - 'prancing', - 'pranker', - 'prankish', - 'prankster', - 'prayer', - 'praying', - 'preacher', - 'preaching', - 'preachy', - 'preamble', - 'precinct', - 'precise', - 'precision', - 'precook', - 'precut', - 'predator', - 'predefine', - 'predict', - 'preface', - 'prefix', - 'preflight', - 'preformed', - 'pregame', - 'pregnancy', - 'pregnant', - 'preheated', - 'prelaunch', - 'prelaw', - 'prelude', - 'premiere', - 'premises', - 'premium', - 'prenatal', - 'preoccupy', - 'preorder', - 'prepaid', - 'prepay', - 'preplan', - 'preppy', - 'preschool', - 'prescribe', - 'preseason', - 'preset', - 'preshow', - 'president', - 'presoak', - 'press', - 'presume', - 'presuming', - 'preteen', - 'pretended', - 'pretender', - 'pretense', - 'pretext', - 'pretty', - 'pretzel', - 'prevail', - 'prevalent', - 'prevent', - 'preview', - 'previous', - 'prewar', - 'prewashed', - 'prideful', - 'pried', - 'primal', - 'primarily', - 'primary', - 'primate', - 'primer', - 'primp', - 'princess', - 'print', - 'prior', - 'prism', - 'prison', - 'prissy', - 'pristine', - 'privacy', - 'private', - 'privatize', - 'prize', - 'proactive', - 'probable', - 'probably', - 'probation', - 'probe', - 'probing', - 'probiotic', - 'problem', - 'procedure', - 'process', - 'proclaim', - 'procreate', - 'procurer', - 'prodigal', - 'prodigy', - 'produce', - 'product', - 'profane', - 'profanity', - 'professed', - 'professor', - 'profile', - 'profound', - 'profusely', - 'progeny', - 'prognosis', - 'program', - 'progress', - 'projector', - 'prologue', - 'prolonged', - 'promenade', - 'prominent', - 'promoter', - 'promotion', - 'prompter', - 'promptly', - 'prone', - 'prong', - 'pronounce', - 'pronto', - 'proofing', - 'proofread', - 'proofs', - 'propeller', - 'properly', - 'property', - 'proponent', - 'proposal', - 'propose', - 'props', - 'prorate', - 'protector', - 'protegee', - 'proton', - 'prototype', - 'protozoan', - 'protract', - 'protrude', - 'proud', - 'provable', - 'proved', - 'proven', - 'provided', - 'provider', - 'providing', - 'province', - 'proving', - 'provoke', - 'provoking', - 'provolone', - 'prowess', - 'prowler', - 'prowling', - 'proximity', - 'proxy', - 'prozac', - 'prude', - 'prudishly', - 'prune', - 'pruning', - 'pry', - 'psychic', - 'public', - 'publisher', - 'pucker', - 'pueblo', - 'pug', - 'pull', - 'pulmonary', - 'pulp', - 'pulsate', - 'pulse', - 'pulverize', - 'puma', - 'pumice', - 'pummel', - 'punch', - 'punctual', - 'punctuate', - 'punctured', - 'pungent', - 'punisher', - 'punk', - 'pupil', - 'puppet', - 'puppy', - 'purchase', - 'pureblood', - 'purebred', - 'purely', - 'pureness', - 'purgatory', - 'purge', - 'purging', - 'purifier', - 'purify', - 'purist', - 'puritan', - 'purity', - 'purple', - 'purplish', - 'purposely', - 'purr', - 'purse', - 'pursuable', - 'pursuant', - 'pursuit', - 'purveyor', - 'pushcart', - 'pushchair', - 'pusher', - 'pushiness', - 'pushing', - 'pushover', - 'pushpin', - 'pushup', - 'pushy', - 'putdown', - 'putt', - 'puzzle', - 'puzzling', - 'pyramid', - 'pyromania', - 'python', - 'quack', - 'quadrant', - 'quail', - 'quaintly', - 'quake', - 'quaking', - 'qualified', - 'qualifier', - 'qualify', - 'quality', - 'qualm', - 'quantum', - 'quarrel', - 'quarry', - 'quartered', - 'quarterly', - 'quarters', - 'quartet', - 'quench', - 'query', - 'quicken', - 'quickly', - 'quickness', - 'quicksand', - 'quickstep', - 'quiet', - 'quill', - 'quilt', - 'quintet', - 'quintuple', - 'quirk', - 'quit', - 'quiver', - 'quizzical', - 'quotable', - 'quotation', - 'quote', - 'rabid', - 'race', - 'racing', - 'racism', - 'rack', - 'racoon', - 'radar', - 'radial', - 'radiance', - 'radiantly', - 'radiated', - 'radiation', - 'radiator', - 'radio', - 'radish', - 'raffle', - 'raft', - 'rage', - 'ragged', - 'raging', - 'ragweed', - 'raider', - 'railcar', - 'railing', - 'railroad', - 'railway', - 'raisin', - 'rake', - 'raking', - 'rally', - 'ramble', - 'rambling', - 'ramp', - 'ramrod', - 'ranch', - 'rancidity', - 'random', - 'ranged', - 'ranger', - 'ranging', - 'ranked', - 'ranking', - 'ransack', - 'ranting', - 'rants', - 'rare', - 'rarity', - 'rascal', - 'rash', - 'rasping', - 'ravage', - 'raven', - 'ravine', - 'raving', - 'ravioli', - 'ravishing', - 'reabsorb', - 'reach', - 'reacquire', - 'reaction', - 'reactive', - 'reactor', - 'reaffirm', - 'ream', - 'reanalyze', - 'reappear', - 'reapply', - 'reappoint', - 'reapprove', - 'rearrange', - 'rearview', - 'reason', - 'reassign', - 'reassure', - 'reattach', - 'reawake', - 'rebalance', - 'rebate', - 'rebel', - 'rebirth', - 'reboot', - 'reborn', - 'rebound', - 'rebuff', - 'rebuild', - 'rebuilt', - 'reburial', - 'rebuttal', - 'recall', - 'recant', - 'recapture', - 'recast', - 'recede', - 'recent', - 'recess', - 'recharger', - 'recipient', - 'recital', - 'recite', - 'reckless', - 'reclaim', - 'recliner', - 'reclining', - 'recluse', - 'reclusive', - 'recognize', - 'recoil', - 'recollect', - 'recolor', - 'reconcile', - 'reconfirm', - 'reconvene', - 'recopy', - 'record', - 'recount', - 'recoup', - 'recovery', - 'recreate', - 'rectal', - 'rectangle', - 'rectified', - 'rectify', - 'recycled', - 'recycler', - 'recycling', - 'reemerge', - 'reenact', - 'reenter', - 'reentry', - 'reexamine', - 'referable', - 'referee', - 'reference', - 'refill', - 'refinance', - 'refined', - 'refinery', - 'refining', - 'refinish', - 'reflected', - 'reflector', - 'reflex', - 'reflux', - 'refocus', - 'refold', - 'reforest', - 'reformat', - 'reformed', - 'reformer', - 'reformist', - 'refract', - 'refrain', - 'refreeze', - 'refresh', - 'refried', - 'refueling', - 'refund', - 'refurbish', - 'refurnish', - 'refusal', - 'refuse', - 'refusing', - 'refutable', - 'refute', - 'regain', - 'regalia', - 'regally', - 'reggae', - 'regime', - 'region', - 'register', - 'registrar', - 'registry', - 'regress', - 'regretful', - 'regroup', - 'regular', - 'regulate', - 'regulator', - 'rehab', - 'reheat', - 'rehire', - 'rehydrate', - 'reimburse', - 'reissue', - 'reiterate', - 'rejoice', - 'rejoicing', - 'rejoin', - 'rekindle', - 'relapse', - 'relapsing', - 'relatable', - 'related', - 'relation', - 'relative', - 'relax', - 'relay', - 'relearn', - 'release', - 'relenting', - 'reliable', - 'reliably', - 'reliance', - 'reliant', - 'relic', - 'relieve', - 'relieving', - 'relight', - 'relish', - 'relive', - 'reload', - 'relocate', - 'relock', - 'reluctant', - 'rely', - 'remake', - 'remark', - 'remarry', - 'rematch', - 'remedial', - 'remedy', - 'remember', - 'reminder', - 'remindful', - 'remission', - 'remix', - 'remnant', - 'remodeler', - 'remold', - 'remorse', - 'remote', - 'removable', - 'removal', - 'removed', - 'remover', - 'removing', - 'rename', - 'renderer', - 'rendering', - 'rendition', - 'renegade', - 'renewable', - 'renewably', - 'renewal', - 'renewed', - 'renounce', - 'renovate', - 'renovator', - 'rentable', - 'rental', - 'rented', - 'renter', - 'reoccupy', - 'reoccur', - 'reopen', - 'reorder', - 'repackage', - 'repacking', - 'repaint', - 'repair', - 'repave', - 'repaying', - 'repayment', - 'repeal', - 'repeated', - 'repeater', - 'repent', - 'rephrase', - 'replace', - 'replay', - 'replica', - 'reply', - 'reporter', - 'repose', - 'repossess', - 'repost', - 'repressed', - 'reprimand', - 'reprint', - 'reprise', - 'reproach', - 'reprocess', - 'reproduce', - 'reprogram', - 'reps', - 'reptile', - 'reptilian', - 'repugnant', - 'repulsion', - 'repulsive', - 'repurpose', - 'reputable', - 'reputably', - 'request', - 'require', - 'requisite', - 'reroute', - 'rerun', - 'resale', - 'resample', - 'rescuer', - 'reseal', - 'research', - 'reselect', - 'reseller', - 'resemble', - 'resend', - 'resent', - 'reset', - 'reshape', - 'reshoot', - 'reshuffle', - 'residence', - 'residency', - 'resident', - 'residual', - 'residue', - 'resigned', - 'resilient', - 'resistant', - 'resisting', - 'resize', - 'resolute', - 'resolved', - 'resonant', - 'resonate', - 'resort', - 'resource', - 'respect', - 'resubmit', - 'result', - 'resume', - 'resupply', - 'resurface', - 'resurrect', - 'retail', - 'retainer', - 'retaining', - 'retake', - 'retaliate', - 'retention', - 'rethink', - 'retinal', - 'retired', - 'retiree', - 'retiring', - 'retold', - 'retool', - 'retorted', - 'retouch', - 'retrace', - 'retract', - 'retrain', - 'retread', - 'retreat', - 'retrial', - 'retrieval', - 'retriever', - 'retry', - 'return', - 'retying', - 'retype', - 'reunion', - 'reunite', - 'reusable', - 'reuse', - 'reveal', - 'reveler', - 'revenge', - 'revenue', - 'reverb', - 'revered', - 'reverence', - 'reverend', - 'reversal', - 'reverse', - 'reversing', - 'reversion', - 'revert', - 'revisable', - 'revise', - 'revision', - 'revisit', - 'revivable', - 'revival', - 'reviver', - 'reviving', - 'revocable', - 'revoke', - 'revolt', - 'revolver', - 'revolving', - 'reward', - 'rewash', - 'rewind', - 'rewire', - 'reword', - 'rework', - 'rewrap', - 'rewrite', - 'rhyme', - 'ribbon', - 'ribcage', - 'rice', - 'riches', - 'richly', - 'richness', - 'rickety', - 'ricotta', - 'riddance', - 'ridden', - 'ride', - 'riding', - 'rifling', - 'rift', - 'rigging', - 'rigid', - 'rigor', - 'rimless', - 'rimmed', - 'rind', - 'rink', - 'rinse', - 'rinsing', - 'riot', - 'ripcord', - 'ripeness', - 'ripening', - 'ripping', - 'ripple', - 'rippling', - 'riptide', - 'rise', - 'rising', - 'risk', - 'risotto', - 'ritalin', - 'ritzy', - 'rival', - 'riverbank', - 'riverbed', - 'riverboat', - 'riverside', - 'riveter', - 'riveting', - 'roamer', - 'roaming', - 'roast', - 'robbing', - 'robe', - 'robin', - 'robotics', - 'robust', - 'rockband', - 'rocker', - 'rocket', - 'rockfish', - 'rockiness', - 'rocking', - 'rocklike', - 'rockslide', - 'rockstar', - 'rocky', - 'rogue', - 'roman', - 'romp', - 'rope', - 'roping', - 'roster', - 'rosy', - 'rotten', - 'rotting', - 'rotunda', - 'roulette', - 'rounding', - 'roundish', - 'roundness', - 'roundup', - 'roundworm', - 'routine', - 'routing', - 'rover', - 'roving', - 'royal', - 'rubbed', - 'rubber', - 'rubbing', - 'rubble', - 'rubdown', - 'ruby', - 'ruckus', - 'rudder', - 'rug', - 'ruined', - 'rule', - 'rumble', - 'rumbling', - 'rummage', - 'rumor', - 'runaround', - 'rundown', - 'runner', - 'running', - 'runny', - 'runt', - 'runway', - 'rupture', - 'rural', - 'ruse', - 'rush', - 'rust', - 'rut', - 'sabbath', - 'sabotage', - 'sacrament', - 'sacred', - 'sacrifice', - 'sadden', - 'saddlebag', - 'saddled', - 'saddling', - 'sadly', - 'sadness', - 'safari', - 'safeguard', - 'safehouse', - 'safely', - 'safeness', - 'saffron', - 'saga', - 'sage', - 'sagging', - 'saggy', - 'said', - 'saint', - 'sake', - 'salad', - 'salami', - 'salaried', - 'salary', - 'saline', - 'salon', - 'saloon', - 'salsa', - 'salt', - 'salutary', - 'salute', - 'salvage', - 'salvaging', - 'salvation', - 'same', - 'sample', - 'sampling', - 'sanction', - 'sanctity', - 'sanctuary', - 'sandal', - 'sandbag', - 'sandbank', - 'sandbar', - 'sandblast', - 'sandbox', - 'sanded', - 'sandfish', - 'sanding', - 'sandlot', - 'sandpaper', - 'sandpit', - 'sandstone', - 'sandstorm', - 'sandworm', - 'sandy', - 'sanitary', - 'sanitizer', - 'sank', - 'santa', - 'sapling', - 'sappiness', - 'sappy', - 'sarcasm', - 'sarcastic', - 'sardine', - 'sash', - 'sasquatch', - 'sassy', - 'satchel', - 'satiable', - 'satin', - 'satirical', - 'satisfied', - 'satisfy', - 'saturate', - 'saturday', - 'sauciness', - 'saucy', - 'sauna', - 'savage', - 'savanna', - 'saved', - 'savings', - 'savior', - 'savor', - 'saxophone', - 'say', - 'scabbed', - 'scabby', - 'scalded', - 'scalding', - 'scale', - 'scaling', - 'scallion', - 'scallop', - 'scalping', - 'scam', - 'scandal', - 'scanner', - 'scanning', - 'scant', - 'scapegoat', - 'scarce', - 'scarcity', - 'scarecrow', - 'scared', - 'scarf', - 'scarily', - 'scariness', - 'scarring', - 'scary', - 'scavenger', - 'scenic', - 'schedule', - 'schematic', - 'scheme', - 'scheming', - 'schilling', - 'schnapps', - 'scholar', - 'science', - 'scientist', - 'scion', - 'scoff', - 'scolding', - 'scone', - 'scoop', - 'scooter', - 'scope', - 'scorch', - 'scorebook', - 'scorecard', - 'scored', - 'scoreless', - 'scorer', - 'scoring', - 'scorn', - 'scorpion', - 'scotch', - 'scoundrel', - 'scoured', - 'scouring', - 'scouting', - 'scouts', - 'scowling', - 'scrabble', - 'scraggly', - 'scrambled', - 'scrambler', - 'scrap', - 'scratch', - 'scrawny', - 'screen', - 'scribble', - 'scribe', - 'scribing', - 'scrimmage', - 'script', - 'scroll', - 'scrooge', - 'scrounger', - 'scrubbed', - 'scrubber', - 'scruffy', - 'scrunch', - 'scrutiny', - 'scuba', - 'scuff', - 'sculptor', - 'sculpture', - 'scurvy', - 'scuttle', - 'secluded', - 'secluding', - 'seclusion', - 'second', - 'secrecy', - 'secret', - 'sectional', - 'sector', - 'secular', - 'securely', - 'security', - 'sedan', - 'sedate', - 'sedation', - 'sedative', - 'sediment', - 'seduce', - 'seducing', - 'segment', - 'seismic', - 'seizing', - 'seldom', - 'selected', - 'selection', - 'selective', - 'selector', - 'self', - 'seltzer', - 'semantic', - 'semester', - 'semicolon', - 'semifinal', - 'seminar', - 'semisoft', - 'semisweet', - 'senate', - 'senator', - 'send', - 'senior', - 'senorita', - 'sensation', - 'sensitive', - 'sensitize', - 'sensually', - 'sensuous', - 'sepia', - 'september', - 'septic', - 'septum', - 'sequel', - 'sequence', - 'sequester', - 'series', - 'sermon', - 'serotonin', - 'serpent', - 'serrated', - 'serve', - 'service', - 'serving', - 'sesame', - 'sessions', - 'setback', - 'setting', - 'settle', - 'settling', - 'setup', - 'sevenfold', - 'seventeen', - 'seventh', - 'seventy', - 'severity', - 'shabby', - 'shack', - 'shaded', - 'shadily', - 'shadiness', - 'shading', - 'shadow', - 'shady', - 'shaft', - 'shakable', - 'shakily', - 'shakiness', - 'shaking', - 'shaky', - 'shale', - 'shallot', - 'shallow', - 'shame', - 'shampoo', - 'shamrock', - 'shank', - 'shanty', - 'shape', - 'shaping', - 'share', - 'sharpener', - 'sharper', - 'sharpie', - 'sharply', - 'sharpness', - 'shawl', - 'sheath', - 'shed', - 'sheep', - 'sheet', - 'shelf', - 'shell', - 'shelter', - 'shelve', - 'shelving', - 'sherry', - 'shield', - 'shifter', - 'shifting', - 'shiftless', - 'shifty', - 'shimmer', - 'shimmy', - 'shindig', - 'shine', - 'shingle', - 'shininess', - 'shining', - 'shiny', - 'ship', - 'shirt', - 'shivering', - 'shock', - 'shone', - 'shoplift', - 'shopper', - 'shopping', - 'shoptalk', - 'shore', - 'shortage', - 'shortcake', - 'shortcut', - 'shorten', - 'shorter', - 'shorthand', - 'shortlist', - 'shortly', - 'shortness', - 'shorts', - 'shortwave', - 'shorty', - 'shout', - 'shove', - 'showbiz', - 'showcase', - 'showdown', - 'shower', - 'showgirl', - 'showing', - 'showman', - 'shown', - 'showoff', - 'showpiece', - 'showplace', - 'showroom', - 'showy', - 'shrank', - 'shrapnel', - 'shredder', - 'shredding', - 'shrewdly', - 'shriek', - 'shrill', - 'shrimp', - 'shrine', - 'shrink', - 'shrivel', - 'shrouded', - 'shrubbery', - 'shrubs', - 'shrug', - 'shrunk', - 'shucking', - 'shudder', - 'shuffle', - 'shuffling', - 'shun', - 'shush', - 'shut', - 'shy', - 'siamese', - 'siberian', - 'sibling', - 'siding', - 'sierra', - 'siesta', - 'sift', - 'sighing', - 'silenced', - 'silencer', - 'silent', - 'silica', - 'silicon', - 'silk', - 'silliness', - 'silly', - 'silo', - 'silt', - 'silver', - 'similarly', - 'simile', - 'simmering', - 'simple', - 'simplify', - 'simply', - 'sincere', - 'sincerity', - 'singer', - 'singing', - 'single', - 'singular', - 'sinister', - 'sinless', - 'sinner', - 'sinuous', - 'sip', - 'siren', - 'sister', - 'sitcom', - 'sitter', - 'sitting', - 'situated', - 'situation', - 'sixfold', - 'sixteen', - 'sixth', - 'sixties', - 'sixtieth', - 'sixtyfold', - 'sizable', - 'sizably', - 'size', - 'sizing', - 'sizzle', - 'sizzling', - 'skater', - 'skating', - 'skedaddle', - 'skeletal', - 'skeleton', - 'skeptic', - 'sketch', - 'skewed', - 'skewer', - 'skid', - 'skied', - 'skier', - 'skies', - 'skiing', - 'skilled', - 'skillet', - 'skillful', - 'skimmed', - 'skimmer', - 'skimming', - 'skimpily', - 'skincare', - 'skinhead', - 'skinless', - 'skinning', - 'skinny', - 'skintight', - 'skipper', - 'skipping', - 'skirmish', - 'skirt', - 'skittle', - 'skydiver', - 'skylight', - 'skyline', - 'skype', - 'skyrocket', - 'skyward', - 'slab', - 'slacked', - 'slacker', - 'slacking', - 'slackness', - 'slacks', - 'slain', - 'slam', - 'slander', - 'slang', - 'slapping', - 'slapstick', - 'slashed', - 'slashing', - 'slate', - 'slather', - 'slaw', - 'sled', - 'sleek', - 'sleep', - 'sleet', - 'sleeve', - 'slept', - 'sliceable', - 'sliced', - 'slicer', - 'slicing', - 'slick', - 'slider', - 'slideshow', - 'sliding', - 'slighted', - 'slighting', - 'slightly', - 'slimness', - 'slimy', - 'slinging', - 'slingshot', - 'slinky', - 'slip', - 'slit', - 'sliver', - 'slobbery', - 'slogan', - 'sloped', - 'sloping', - 'sloppily', - 'sloppy', - 'slot', - 'slouching', - 'slouchy', - 'sludge', - 'slug', - 'slum', - 'slurp', - 'slush', - 'sly', - 'small', - 'smartly', - 'smartness', - 'smasher', - 'smashing', - 'smashup', - 'smell', - 'smelting', - 'smile', - 'smilingly', - 'smirk', - 'smite', - 'smith', - 'smitten', - 'smock', - 'smog', - 'smoked', - 'smokeless', - 'smokiness', - 'smoking', - 'smoky', - 'smolder', - 'smooth', - 'smother', - 'smudge', - 'smudgy', - 'smuggler', - 'smuggling', - 'smugly', - 'smugness', - 'snack', - 'snagged', - 'snaking', - 'snap', - 'snare', - 'snarl', - 'snazzy', - 'sneak', - 'sneer', - 'sneeze', - 'sneezing', - 'snide', - 'sniff', - 'snippet', - 'snipping', - 'snitch', - 'snooper', - 'snooze', - 'snore', - 'snoring', - 'snorkel', - 'snort', - 'snout', - 'snowbird', - 'snowboard', - 'snowbound', - 'snowcap', - 'snowdrift', - 'snowdrop', - 'snowfall', - 'snowfield', - 'snowflake', - 'snowiness', - 'snowless', - 'snowman', - 'snowplow', - 'snowshoe', - 'snowstorm', - 'snowsuit', - 'snowy', - 'snub', - 'snuff', - 'snuggle', - 'snugly', - 'snugness', - 'speak', - 'spearfish', - 'spearhead', - 'spearman', - 'spearmint', - 'species', - 'specimen', - 'specked', - 'speckled', - 'specks', - 'spectacle', - 'spectator', - 'spectrum', - 'speculate', - 'speech', - 'speed', - 'spellbind', - 'speller', - 'spelling', - 'spendable', - 'spender', - 'spending', - 'spent', - 'spew', - 'sphere', - 'spherical', - 'sphinx', - 'spider', - 'spied', - 'spiffy', - 'spill', - 'spilt', - 'spinach', - 'spinal', - 'spindle', - 'spinner', - 'spinning', - 'spinout', - 'spinster', - 'spiny', - 'spiral', - 'spirited', - 'spiritism', - 'spirits', - 'spiritual', - 'splashed', - 'splashing', - 'splashy', - 'splatter', - 'spleen', - 'splendid', - 'splendor', - 'splice', - 'splicing', - 'splinter', - 'splotchy', - 'splurge', - 'spoilage', - 'spoiled', - 'spoiler', - 'spoiling', - 'spoils', - 'spoken', - 'spokesman', - 'sponge', - 'spongy', - 'sponsor', - 'spoof', - 'spookily', - 'spooky', - 'spool', - 'spoon', - 'spore', - 'sporting', - 'sports', - 'sporty', - 'spotless', - 'spotlight', - 'spotted', - 'spotter', - 'spotting', - 'spotty', - 'spousal', - 'spouse', - 'spout', - 'sprain', - 'sprang', - 'sprawl', - 'spray', - 'spree', - 'sprig', - 'spring', - 'sprinkled', - 'sprinkler', - 'sprint', - 'sprite', - 'sprout', - 'spruce', - 'sprung', - 'spry', - 'spud', - 'spur', - 'sputter', - 'spyglass', - 'squabble', - 'squad', - 'squall', - 'squander', - 'squash', - 'squatted', - 'squatter', - 'squatting', - 'squeak', - 'squealer', - 'squealing', - 'squeamish', - 'squeegee', - 'squeeze', - 'squeezing', - 'squid', - 'squiggle', - 'squiggly', - 'squint', - 'squire', - 'squirt', - 'squishier', - 'squishy', - 'stability', - 'stabilize', - 'stable', - 'stack', - 'stadium', - 'staff', - 'stage', - 'staging', - 'stagnant', - 'stagnate', - 'stainable', - 'stained', - 'staining', - 'stainless', - 'stalemate', - 'staleness', - 'stalling', - 'stallion', - 'stamina', - 'stammer', - 'stamp', - 'stand', - 'stank', - 'staple', - 'stapling', - 'starboard', - 'starch', - 'stardom', - 'stardust', - 'starfish', - 'stargazer', - 'staring', - 'stark', - 'starless', - 'starlet', - 'starlight', - 'starlit', - 'starring', - 'starry', - 'starship', - 'starter', - 'starting', - 'startle', - 'startling', - 'startup', - 'starved', - 'starving', - 'stash', - 'state', - 'static', - 'statistic', - 'statue', - 'stature', - 'status', - 'statute', - 'statutory', - 'staunch', - 'stays', - 'steadfast', - 'steadier', - 'steadily', - 'steadying', - 'steam', - 'steed', - 'steep', - 'steerable', - 'steering', - 'steersman', - 'stegosaur', - 'stellar', - 'stem', - 'stench', - 'stencil', - 'step', - 'stereo', - 'sterile', - 'sterility', - 'sterilize', - 'sterling', - 'sternness', - 'sternum', - 'stew', - 'stick', - 'stiffen', - 'stiffly', - 'stiffness', - 'stifle', - 'stifling', - 'stillness', - 'stilt', - 'stimulant', - 'stimulate', - 'stimuli', - 'stimulus', - 'stinger', - 'stingily', - 'stinging', - 'stingray', - 'stingy', - 'stinking', - 'stinky', - 'stipend', - 'stipulate', - 'stir', - 'stitch', - 'stock', - 'stoic', - 'stoke', - 'stole', - 'stomp', - 'stonewall', - 'stoneware', - 'stonework', - 'stoning', - 'stony', - 'stood', - 'stooge', - 'stool', - 'stoop', - 'stoplight', - 'stoppable', - 'stoppage', - 'stopped', - 'stopper', - 'stopping', - 'stopwatch', - 'storable', - 'storage', - 'storeroom', - 'storewide', - 'storm', - 'stout', - 'stove', - 'stowaway', - 'stowing', - 'straddle', - 'straggler', - 'strained', - 'strainer', - 'straining', - 'strangely', - 'stranger', - 'strangle', - 'strategic', - 'strategy', - 'stratus', - 'straw', - 'stray', - 'streak', - 'stream', - 'street', - 'strength', - 'strenuous', - 'strep', - 'stress', - 'stretch', - 'strewn', - 'stricken', - 'strict', - 'stride', - 'strife', - 'strike', - 'striking', - 'strive', - 'striving', - 'strobe', - 'strode', - 'stroller', - 'strongbox', - 'strongly', - 'strongman', - 'struck', - 'structure', - 'strudel', - 'struggle', - 'strum', - 'strung', - 'strut', - 'stubbed', - 'stubble', - 'stubbly', - 'stubborn', - 'stucco', - 'stuck', - 'student', - 'studied', - 'studio', - 'study', - 'stuffed', - 'stuffing', - 'stuffy', - 'stumble', - 'stumbling', - 'stump', - 'stung', - 'stunned', - 'stunner', - 'stunning', - 'stunt', - 'stupor', - 'sturdily', - 'sturdy', - 'styling', - 'stylishly', - 'stylist', - 'stylized', - 'stylus', - 'suave', - 'subarctic', - 'subatomic', - 'subdivide', - 'subdued', - 'subduing', - 'subfloor', - 'subgroup', - 'subheader', - 'subject', - 'sublease', - 'sublet', - 'sublevel', - 'sublime', - 'submarine', - 'submerge', - 'submersed', - 'submitter', - 'subpanel', - 'subpar', - 'subplot', - 'subprime', - 'subscribe', - 'subscript', - 'subsector', - 'subside', - 'subsiding', - 'subsidize', - 'subsidy', - 'subsoil', - 'subsonic', - 'substance', - 'subsystem', - 'subtext', - 'subtitle', - 'subtly', - 'subtotal', - 'subtract', - 'subtype', - 'suburb', - 'subway', - 'subwoofer', - 'subzero', - 'succulent', - 'such', - 'suction', - 'sudden', - 'sudoku', - 'suds', - 'sufferer', - 'suffering', - 'suffice', - 'suffix', - 'suffocate', - 'suffrage', - 'sugar', - 'suggest', - 'suing', - 'suitable', - 'suitably', - 'suitcase', - 'suitor', - 'sulfate', - 'sulfide', - 'sulfite', - 'sulfur', - 'sulk', - 'sullen', - 'sulphate', - 'sulphuric', - 'sultry', - 'superbowl', - 'superglue', - 'superhero', - 'superior', - 'superjet', - 'superman', - 'supermom', - 'supernova', - 'supervise', - 'supper', - 'supplier', - 'supply', - 'support', - 'supremacy', - 'supreme', - 'surcharge', - 'surely', - 'sureness', - 'surface', - 'surfacing', - 'surfboard', - 'surfer', - 'surgery', - 'surgical', - 'surging', - 'surname', - 'surpass', - 'surplus', - 'surprise', - 'surreal', - 'surrender', - 'surrogate', - 'surround', - 'survey', - 'survival', - 'survive', - 'surviving', - 'survivor', - 'sushi', - 'suspect', - 'suspend', - 'suspense', - 'sustained', - 'sustainer', - 'swab', - 'swaddling', - 'swagger', - 'swampland', - 'swan', - 'swapping', - 'swarm', - 'sway', - 'swear', - 'sweat', - 'sweep', - 'swell', - 'swept', - 'swerve', - 'swifter', - 'swiftly', - 'swiftness', - 'swimmable', - 'swimmer', - 'swimming', - 'swimsuit', - 'swimwear', - 'swinger', - 'swinging', - 'swipe', - 'swirl', - 'switch', - 'swivel', - 'swizzle', - 'swooned', - 'swoop', - 'swoosh', - 'swore', - 'sworn', - 'swung', - 'sycamore', - 'sympathy', - 'symphonic', - 'symphony', - 'symptom', - 'synapse', - 'syndrome', - 'synergy', - 'synopses', - 'synopsis', - 'synthesis', - 'synthetic', - 'syrup', - 'system', - 't-shirt', - 'tabasco', - 'tabby', - 'tableful', - 'tables', - 'tablet', - 'tableware', - 'tabloid', - 'tackiness', - 'tacking', - 'tackle', - 'tackling', - 'tacky', - 'taco', - 'tactful', - 'tactical', - 'tactics', - 'tactile', - 'tactless', - 'tadpole', - 'taekwondo', - 'tag', - 'tainted', - 'take', - 'taking', - 'talcum', - 'talisman', - 'tall', - 'talon', - 'tamale', - 'tameness', - 'tamer', - 'tamper', - 'tank', - 'tanned', - 'tannery', - 'tanning', - 'tantrum', - 'tapeless', - 'tapered', - 'tapering', - 'tapestry', - 'tapioca', - 'tapping', - 'taps', - 'tarantula', - 'target', - 'tarmac', - 'tarnish', - 'tarot', - 'tartar', - 'tartly', - 'tartness', - 'task', - 'tassel', - 'taste', - 'tastiness', - 'tasting', - 'tasty', - 'tattered', - 'tattle', - 'tattling', - 'tattoo', - 'taunt', - 'tavern', - 'thank', - 'that', - 'thaw', - 'theater', - 'theatrics', - 'thee', - 'theft', - 'theme', - 'theology', - 'theorize', - 'thermal', - 'thermos', - 'thesaurus', - 'these', - 'thesis', - 'thespian', - 'thicken', - 'thicket', - 'thickness', - 'thieving', - 'thievish', - 'thigh', - 'thimble', - 'thing', - 'think', - 'thinly', - 'thinner', - 'thinness', - 'thinning', - 'thirstily', - 'thirsting', - 'thirsty', - 'thirteen', - 'thirty', - 'thong', - 'thorn', - 'those', - 'thousand', - 'thrash', - 'thread', - 'threaten', - 'threefold', - 'thrift', - 'thrill', - 'thrive', - 'thriving', - 'throat', - 'throbbing', - 'throng', - 'throttle', - 'throwaway', - 'throwback', - 'thrower', - 'throwing', - 'thud', - 'thumb', - 'thumping', - 'thursday', - 'thus', - 'thwarting', - 'thyself', - 'tiara', - 'tibia', - 'tidal', - 'tidbit', - 'tidiness', - 'tidings', - 'tidy', - 'tiger', - 'tighten', - 'tightly', - 'tightness', - 'tightrope', - 'tightwad', - 'tigress', - 'tile', - 'tiling', - 'till', - 'tilt', - 'timid', - 'timing', - 'timothy', - 'tinderbox', - 'tinfoil', - 'tingle', - 'tingling', - 'tingly', - 'tinker', - 'tinkling', - 'tinsel', - 'tinsmith', - 'tint', - 'tinwork', - 'tiny', - 'tipoff', - 'tipped', - 'tipper', - 'tipping', - 'tiptoeing', - 'tiptop', - 'tiring', - 'tissue', - 'trace', - 'tracing', - 'track', - 'traction', - 'tractor', - 'trade', - 'trading', - 'tradition', - 'traffic', - 'tragedy', - 'trailing', - 'trailside', - 'train', - 'traitor', - 'trance', - 'tranquil', - 'transfer', - 'transform', - 'translate', - 'transpire', - 'transport', - 'transpose', - 'trapdoor', - 'trapeze', - 'trapezoid', - 'trapped', - 'trapper', - 'trapping', - 'traps', - 'trash', - 'travel', - 'traverse', - 'travesty', - 'tray', - 'treachery', - 'treading', - 'treadmill', - 'treason', - 'treat', - 'treble', - 'tree', - 'trekker', - 'tremble', - 'trembling', - 'tremor', - 'trench', - 'trend', - 'trespass', - 'triage', - 'trial', - 'triangle', - 'tribesman', - 'tribunal', - 'tribune', - 'tributary', - 'tribute', - 'triceps', - 'trickery', - 'trickily', - 'tricking', - 'trickle', - 'trickster', - 'tricky', - 'tricolor', - 'tricycle', - 'trident', - 'tried', - 'trifle', - 'trifocals', - 'trillion', - 'trilogy', - 'trimester', - 'trimmer', - 'trimming', - 'trimness', - 'trinity', - 'trio', - 'tripod', - 'tripping', - 'triumph', - 'trivial', - 'trodden', - 'trolling', - 'trombone', - 'trophy', - 'tropical', - 'tropics', - 'trouble', - 'troubling', - 'trough', - 'trousers', - 'trout', - 'trowel', - 'truce', - 'truck', - 'truffle', - 'trump', - 'trunks', - 'trustable', - 'trustee', - 'trustful', - 'trusting', - 'trustless', - 'truth', - 'try', - 'tubby', - 'tubeless', - 'tubular', - 'tucking', - 'tuesday', - 'tug', - 'tuition', - 'tulip', - 'tumble', - 'tumbling', - 'tummy', - 'turban', - 'turbine', - 'turbofan', - 'turbojet', - 'turbulent', - 'turf', - 'turkey', - 'turmoil', - 'turret', - 'turtle', - 'tusk', - 'tutor', - 'tutu', - 'tux', - 'tweak', - 'tweed', - 'tweet', - 'tweezers', - 'twelve', - 'twentieth', - 'twenty', - 'twerp', - 'twice', - 'twiddle', - 'twiddling', - 'twig', - 'twilight', - 'twine', - 'twins', - 'twirl', - 'twistable', - 'twisted', - 'twister', - 'twisting', - 'twisty', - 'twitch', - 'twitter', - 'tycoon', - 'tying', - 'tyke', - 'udder', - 'ultimate', - 'ultimatum', - 'ultra', - 'umbilical', - 'umbrella', - 'umpire', - 'unabashed', - 'unable', - 'unadorned', - 'unadvised', - 'unafraid', - 'unaired', - 'unaligned', - 'unaltered', - 'unarmored', - 'unashamed', - 'unaudited', - 'unawake', - 'unaware', - 'unbaked', - 'unbalance', - 'unbeaten', - 'unbend', - 'unbent', - 'unbiased', - 'unbitten', - 'unblended', - 'unblessed', - 'unblock', - 'unbolted', - 'unbounded', - 'unboxed', - 'unbraided', - 'unbridle', - 'unbroken', - 'unbuckled', - 'unbundle', - 'unburned', - 'unbutton', - 'uncanny', - 'uncapped', - 'uncaring', - 'uncertain', - 'unchain', - 'unchanged', - 'uncharted', - 'uncheck', - 'uncivil', - 'unclad', - 'unclaimed', - 'unclamped', - 'unclasp', - 'uncle', - 'unclip', - 'uncloak', - 'unclog', - 'unclothed', - 'uncoated', - 'uncoiled', - 'uncolored', - 'uncombed', - 'uncommon', - 'uncooked', - 'uncork', - 'uncorrupt', - 'uncounted', - 'uncouple', - 'uncouth', - 'uncover', - 'uncross', - 'uncrown', - 'uncrushed', - 'uncured', - 'uncurious', - 'uncurled', - 'uncut', - 'undamaged', - 'undated', - 'undaunted', - 'undead', - 'undecided', - 'undefined', - 'underage', - 'underarm', - 'undercoat', - 'undercook', - 'undercut', - 'underdog', - 'underdone', - 'underfed', - 'underfeed', - 'underfoot', - 'undergo', - 'undergrad', - 'underhand', - 'underline', - 'underling', - 'undermine', - 'undermost', - 'underpaid', - 'underpass', - 'underpay', - 'underrate', - 'undertake', - 'undertone', - 'undertook', - 'undertow', - 'underuse', - 'underwear', - 'underwent', - 'underwire', - 'undesired', - 'undiluted', - 'undivided', - 'undocked', - 'undoing', - 'undone', - 'undrafted', - 'undress', - 'undrilled', - 'undusted', - 'undying', - 'unearned', - 'unearth', - 'unease', - 'uneasily', - 'uneasy', - 'uneatable', - 'uneaten', - 'unedited', - 'unelected', - 'unending', - 'unengaged', - 'unenvied', - 'unequal', - 'unethical', - 'uneven', - 'unexpired', - 'unexposed', - 'unfailing', - 'unfair', - 'unfasten', - 'unfazed', - 'unfeeling', - 'unfiled', - 'unfilled', - 'unfitted', - 'unfitting', - 'unfixable', - 'unfixed', - 'unflawed', - 'unfocused', - 'unfold', - 'unfounded', - 'unframed', - 'unfreeze', - 'unfrosted', - 'unfrozen', - 'unfunded', - 'unglazed', - 'ungloved', - 'unglue', - 'ungodly', - 'ungraded', - 'ungreased', - 'unguarded', - 'unguided', - 'unhappily', - 'unhappy', - 'unharmed', - 'unhealthy', - 'unheard', - 'unhearing', - 'unheated', - 'unhelpful', - 'unhidden', - 'unhinge', - 'unhitched', - 'unholy', - 'unhook', - 'unicorn', - 'unicycle', - 'unified', - 'unifier', - 'uniformed', - 'uniformly', - 'unify', - 'unimpeded', - 'uninjured', - 'uninstall', - 'uninsured', - 'uninvited', - 'union', - 'uniquely', - 'unisexual', - 'unison', - 'unissued', - 'unit', - 'universal', - 'universe', - 'unjustly', - 'unkempt', - 'unkind', - 'unknotted', - 'unknowing', - 'unknown', - 'unlaced', - 'unlatch', - 'unlawful', - 'unleaded', - 'unlearned', - 'unleash', - 'unless', - 'unleveled', - 'unlighted', - 'unlikable', - 'unlimited', - 'unlined', - 'unlinked', - 'unlisted', - 'unlit', - 'unlivable', - 'unloaded', - 'unloader', - 'unlocked', - 'unlocking', - 'unlovable', - 'unloved', - 'unlovely', - 'unloving', - 'unluckily', - 'unlucky', - 'unmade', - 'unmanaged', - 'unmanned', - 'unmapped', - 'unmarked', - 'unmasked', - 'unmasking', - 'unmatched', - 'unmindful', - 'unmixable', - 'unmixed', - 'unmolded', - 'unmoral', - 'unmovable', - 'unmoved', - 'unmoving', - 'unnamable', - 'unnamed', - 'unnatural', - 'unneeded', - 'unnerve', - 'unnerving', - 'unnoticed', - 'unopened', - 'unopposed', - 'unpack', - 'unpadded', - 'unpaid', - 'unpainted', - 'unpaired', - 'unpaved', - 'unpeeled', - 'unpicked', - 'unpiloted', - 'unpinned', - 'unplanned', - 'unplanted', - 'unpleased', - 'unpledged', - 'unplowed', - 'unplug', - 'unpopular', - 'unproven', - 'unquote', - 'unranked', - 'unrated', - 'unraveled', - 'unreached', - 'unread', - 'unreal', - 'unreeling', - 'unrefined', - 'unrelated', - 'unrented', - 'unrest', - 'unretired', - 'unrevised', - 'unrigged', - 'unripe', - 'unrivaled', - 'unroasted', - 'unrobed', - 'unroll', - 'unruffled', - 'unruly', - 'unrushed', - 'unsaddle', - 'unsafe', - 'unsaid', - 'unsalted', - 'unsaved', - 'unsavory', - 'unscathed', - 'unscented', - 'unscrew', - 'unsealed', - 'unseated', - 'unsecured', - 'unseeing', - 'unseemly', - 'unseen', - 'unselect', - 'unselfish', - 'unsent', - 'unsettled', - 'unshackle', - 'unshaken', - 'unshaved', - 'unshaven', - 'unsheathe', - 'unshipped', - 'unsightly', - 'unsigned', - 'unskilled', - 'unsliced', - 'unsmooth', - 'unsnap', - 'unsocial', - 'unsoiled', - 'unsold', - 'unsolved', - 'unsorted', - 'unspoiled', - 'unspoken', - 'unstable', - 'unstaffed', - 'unstamped', - 'unsteady', - 'unsterile', - 'unstirred', - 'unstitch', - 'unstopped', - 'unstuck', - 'unstuffed', - 'unstylish', - 'unsubtle', - 'unsubtly', - 'unsuited', - 'unsure', - 'unsworn', - 'untagged', - 'untainted', - 'untaken', - 'untamed', - 'untangled', - 'untapped', - 'untaxed', - 'unthawed', - 'unthread', - 'untidy', - 'untie', - 'until', - 'untimed', - 'untimely', - 'untitled', - 'untoasted', - 'untold', - 'untouched', - 'untracked', - 'untrained', - 'untreated', - 'untried', - 'untrimmed', - 'untrue', - 'untruth', - 'unturned', - 'untwist', - 'untying', - 'unusable', - 'unused', - 'unusual', - 'unvalued', - 'unvaried', - 'unvarying', - 'unveiled', - 'unveiling', - 'unvented', - 'unviable', - 'unvisited', - 'unvocal', - 'unwanted', - 'unwarlike', - 'unwary', - 'unwashed', - 'unwatched', - 'unweave', - 'unwed', - 'unwelcome', - 'unwell', - 'unwieldy', - 'unwilling', - 'unwind', - 'unwired', - 'unwitting', - 'unwomanly', - 'unworldly', - 'unworn', - 'unworried', - 'unworthy', - 'unwound', - 'unwoven', - 'unwrapped', - 'unwritten', - 'unzip', - 'upbeat', - 'upchuck', - 'upcoming', - 'upcountry', - 'update', - 'upfront', - 'upgrade', - 'upheaval', - 'upheld', - 'uphill', - 'uphold', - 'uplifted', - 'uplifting', - 'upload', - 'upon', - 'upper', - 'upright', - 'uprising', - 'upriver', - 'uproar', - 'uproot', - 'upscale', - 'upside', - 'upstage', - 'upstairs', - 'upstart', - 'upstate', - 'upstream', - 'upstroke', - 'upswing', - 'uptake', - 'uptight', - 'uptown', - 'upturned', - 'upward', - 'upwind', - 'uranium', - 'urban', - 'urchin', - 'urethane', - 'urgency', - 'urgent', - 'urging', - 'urologist', - 'urology', - 'usable', - 'usage', - 'useable', - 'used', - 'uselessly', - 'user', - 'usher', - 'usual', - 'utensil', - 'utility', - 'utilize', - 'utmost', - 'utopia', - 'utter', - 'vacancy', - 'vacant', - 'vacate', - 'vacation', - 'vagabond', - 'vagrancy', - 'vagrantly', - 'vaguely', - 'vagueness', - 'valiant', - 'valid', - 'valium', - 'valley', - 'valuables', - 'value', - 'vanilla', - 'vanish', - 'vanity', - 'vanquish', - 'vantage', - 'vaporizer', - 'variable', - 'variably', - 'varied', - 'variety', - 'various', - 'varmint', - 'varnish', - 'varsity', - 'varying', - 'vascular', - 'vaseline', - 'vastly', - 'vastness', - 'veal', - 'vegan', - 'veggie', - 'vehicular', - 'velcro', - 'velocity', - 'velvet', - 'vendetta', - 'vending', - 'vendor', - 'veneering', - 'vengeful', - 'venomous', - 'ventricle', - 'venture', - 'venue', - 'venus', - 'verbalize', - 'verbally', - 'verbose', - 'verdict', - 'verify', - 'verse', - 'version', - 'versus', - 'vertebrae', - 'vertical', - 'vertigo', - 'very', - 'vessel', - 'vest', - 'veteran', - 'veto', - 'vexingly', - 'viability', - 'viable', - 'vibes', - 'vice', - 'vicinity', - 'victory', - 'video', - 'viewable', - 'viewer', - 'viewing', - 'viewless', - 'viewpoint', - 'vigorous', - 'village', - 'villain', - 'vindicate', - 'vineyard', - 'vintage', - 'violate', - 'violation', - 'violator', - 'violet', - 'violin', - 'viper', - 'viral', - 'virtual', - 'virtuous', - 'virus', - 'visa', - 'viscosity', - 'viscous', - 'viselike', - 'visible', - 'visibly', - 'vision', - 'visiting', - 'visitor', - 'visor', - 'vista', - 'vitality', - 'vitalize', - 'vitally', - 'vitamins', - 'vivacious', - 'vividly', - 'vividness', - 'vixen', - 'vocalist', - 'vocalize', - 'vocally', - 'vocation', - 'voice', - 'voicing', - 'void', - 'volatile', - 'volley', - 'voltage', - 'volumes', - 'voter', - 'voting', - 'voucher', - 'vowed', - 'vowel', - 'voyage', - 'wackiness', - 'wad', - 'wafer', - 'waffle', - 'waged', - 'wager', - 'wages', - 'waggle', - 'wagon', - 'wake', - 'waking', - 'walk', - 'walmart', - 'walnut', - 'walrus', - 'waltz', - 'wand', - 'wannabe', - 'wanted', - 'wanting', - 'wasabi', - 'washable', - 'washbasin', - 'washboard', - 'washbowl', - 'washcloth', - 'washday', - 'washed', - 'washer', - 'washhouse', - 'washing', - 'washout', - 'washroom', - 'washstand', - 'washtub', - 'wasp', - 'wasting', - 'watch', - 'water', - 'waviness', - 'waving', - 'wavy', - 'whacking', - 'whacky', - 'wham', - 'wharf', - 'wheat', - 'whenever', - 'whiff', - 'whimsical', - 'whinny', - 'whiny', - 'whisking', - 'whoever', - 'whole', - 'whomever', - 'whoopee', - 'whooping', - 'whoops', - 'why', - 'wick', - 'widely', - 'widen', - 'widget', - 'widow', - 'width', - 'wieldable', - 'wielder', - 'wife', - 'wifi', - 'wikipedia', - 'wildcard', - 'wildcat', - 'wilder', - 'wildfire', - 'wildfowl', - 'wildland', - 'wildlife', - 'wildly', - 'wildness', - 'willed', - 'willfully', - 'willing', - 'willow', - 'willpower', - 'wilt', - 'wimp', - 'wince', - 'wincing', - 'wind', - 'wing', - 'winking', - 'winner', - 'winnings', - 'winter', - 'wipe', - 'wired', - 'wireless', - 'wiring', - 'wiry', - 'wisdom', - 'wise', - 'wish', - 'wisplike', - 'wispy', - 'wistful', - 'wizard', - 'wobble', - 'wobbling', - 'wobbly', - 'wok', - 'wolf', - 'wolverine', - 'womanhood', - 'womankind', - 'womanless', - 'womanlike', - 'womanly', - 'womb', - 'woof', - 'wooing', - 'wool', - 'woozy', - 'word', - 'work', - 'worried', - 'worrier', - 'worrisome', - 'worry', - 'worsening', - 'worshiper', - 'worst', - 'wound', - 'woven', - 'wow', - 'wrangle', - 'wrath', - 'wreath', - 'wreckage', - 'wrecker', - 'wrecking', - 'wrench', - 'wriggle', - 'wriggly', - 'wrinkle', - 'wrinkly', - 'wrist', - 'writing', - 'written', - 'wrongdoer', - 'wronged', - 'wrongful', - 'wrongly', - 'wrongness', - 'wrought', - 'xbox', - 'xerox', - 'yahoo', - 'yam', - 'yanking', - 'yapping', - 'yard', - 'yarn', - 'yeah', - 'yearbook', - 'yearling', - 'yearly', - 'yearning', - 'yeast', - 'yelling', - 'yelp', - 'yen', - 'yesterday', - 'yiddish', - 'yield', - 'yin', - 'yippee', - 'yo-yo', - 'yodel', - 'yoga', - 'yogurt', - 'yonder', - 'yoyo', - 'yummy', - 'zap', - 'zealous', - 'zebra', - 'zen', - 'zeppelin', - 'zero', - 'zestfully', - 'zesty', - 'zigzagged', - 'zipfile', - 'zipping', - 'zippy', - 'zips', - 'zit', - 'zodiac', - 'zombie', - 'zone', - 'zoning', - 'zookeeper', - 'zoologist', - 'zoology', - 'zoom', - ]; + public function GeneratePassword(int $length) : string { + $password = ''; - public function GeneratePassword(int $length) : string { - $password = ''; + for ($i = 0; $i < $length; $i ++) { + if ($password !== '') { + $password = $password . ' '; + } + $password = $password . $this->words[random_int(0, 7775)]; + } - for($i = 0; $i < $length; $i ++) { - if($password !== '') { - $password = $password . ' '; - } - $password = $password . $this->words[random_int(0, 7775)]; - } - - return $password; - } + return $password; + } } - diff --git a/php/src/Container/AioVariables.php b/php/src/Container/AioVariables.php index 10a150f4c3d..13fcea80602 100644 --- a/php/src/Container/AioVariables.php +++ b/php/src/Container/AioVariables.php @@ -3,17 +3,17 @@ namespace AIO\Container; class AioVariables { - /** @var string[] */ - private array $variables = []; + /** @var string[] */ + private array $variables = []; - public function AddVariable(string $variable) : void { - $this->variables[] = $variable; - } + public function AddVariable(string $variable) : void { + $this->variables[] = $variable; + } - /** - * @return string[] - */ - public function GetVariables() : array { - return $this->variables; - } + /** + * @return string[] + */ + public function GetVariables() : array { + return $this->variables; + } } diff --git a/php/src/Container/Container.php b/php/src/Container/Container.php index 9d598b4aa74..3c6d864ccd7 100644 --- a/php/src/Container/Container.php +++ b/php/src/Container/Container.php @@ -3,201 +3,199 @@ namespace AIO\Container; use AIO\Container\State\IContainerState; -use AIO\Data\ConfigurationManager; use AIO\Docker\DockerActionManager; -use AIO\ContainerDefinitionFetcher; class Container { - private string $identifier; - private string $displayName; - private string $containerName; - private string $restartPolicy; - private int $maxShutdownTime; - private ContainerPorts $ports; - private string $internalPorts; - private ContainerVolumes $volumes; - private ContainerEnvironmentVariables $containerEnvironmentVariables; - /** @var string[] */ - private array $dependsOn; - /** @var string[] */ - private array $secrets; - /** @var string[] */ - private array $devices; - /** @var string[] */ - private array $capAdd; - private int $shmSize; - private bool $apparmorUnconfined; - /** @var string[] */ - private array $backupVolumes; - private array $nextcloudExecCommands; - private bool $readOnlyRootFs; - private array $tmpfs; - private bool $init; - private string $imageTag; - private AioVariables $aioVariables; - private string $documentation; - private DockerActionManager $dockerActionManager; - - public function __construct( - string $identifier, - string $displayName, - string $containerName, - string $restartPolicy, - int $maxShutdownTime, - ContainerPorts $ports, - string $internalPorts, - ContainerVolumes $volumes, - ContainerEnvironmentVariables $containerEnvironmentVariables, - array $dependsOn, - array $secrets, - array $devices, - array $capAdd, - int $shmSize, - bool $apparmorUnconfined, - array $backupVolumes, - array $nextcloudExecCommands, - bool $readOnlyRootFs, - array $tmpfs, - bool $init, - string $imageTag, - AioVariables $aioVariables, - string $documentation, - DockerActionManager $dockerActionManager - ) { - $this->identifier = $identifier; - $this->displayName = $displayName; - $this->containerName = $containerName; - $this->restartPolicy = $restartPolicy; - $this->maxShutdownTime = $maxShutdownTime; - $this->ports = $ports; - $this->internalPorts = $internalPorts; - $this->volumes = $volumes; - $this->containerEnvironmentVariables = $containerEnvironmentVariables; - $this->dependsOn = $dependsOn; - $this->secrets = $secrets; - $this->devices = $devices; - $this->capAdd = $capAdd; - $this->shmSize = $shmSize; - $this->apparmorUnconfined = $apparmorUnconfined; - $this->backupVolumes = $backupVolumes; - $this->nextcloudExecCommands = $nextcloudExecCommands; - $this->readOnlyRootFs = $readOnlyRootFs; - $this->tmpfs = $tmpfs; - $this->init = $init; - $this->imageTag = $imageTag; - $this->aioVariables = $aioVariables; - $this->documentation = $documentation; - $this->dockerActionManager = $dockerActionManager; - } - - public function GetIdentifier() : string { - return $this->identifier; - } - - public function GetDisplayName() : string { - return $this->displayName; - } - - public function GetContainerName() : string { - return $this->containerName; - } - - public function GetRestartPolicy() : string { - return $this->restartPolicy; - } - - public function GetImageTag() : string { - return $this->imageTag; - } - - public function GetReadOnlySetting() : bool { - return $this->readOnlyRootFs; - } - - public function GetInit() : bool { - return $this->init; - } - - public function GetShmSize() : int { - return $this->shmSize; - } - - public function isApparmorUnconfined() : bool { - return $this->apparmorUnconfined; - } - - public function GetMaxShutdownTime() : int { - return $this->maxShutdownTime; - } - - public function GetSecrets() : array { - return $this->secrets; - } - - public function GetTmpfs() : array { - return $this->tmpfs; - } - - public function GetDevices() : array { - return $this->devices; - } - - public function GetCapAdds() : array { - return $this->capAdd; - } - - public function GetBackupVolumes() : array { - return $this->backupVolumes; - } - - public function GetPorts() : ContainerPorts { - return $this->ports; - } - - public function GetInternalPort() : string { - return $this->internalPorts; - } - - public function GetVolumes() : ContainerVolumes { - return $this->volumes; - } - - public function GetRunningState() : IContainerState { - return $this->dockerActionManager->GetContainerRunningState($this); - } - - public function GetRestartingState() : IContainerState { - return $this->dockerActionManager->GetContainerRestartingState($this); - } - - public function GetUpdateState() : IContainerState { - return $this->dockerActionManager->GetContainerUpdateState($this); - } - - public function GetStartingState() : IContainerState { - return $this->dockerActionManager->GetContainerStartingState($this); - } - - /** - * @return string[] - */ - public function GetDependsOn() : array { - return $this->dependsOn; - } - - public function GetNextcloudExecCommands() : array { - return $this->nextcloudExecCommands; - } - - public function GetEnvironmentVariables() : ContainerEnvironmentVariables { - return $this->containerEnvironmentVariables; - } - - public function GetAioVariables() : AioVariables { - return $this->aioVariables; - } - - public function GetDocumentation() : string { - return $this->documentation; - } + private string $identifier; + private string $displayName; + private string $containerName; + private string $restartPolicy; + private int $maxShutdownTime; + private ContainerPorts $ports; + private string $internalPorts; + private ContainerVolumes $volumes; + private ContainerEnvironmentVariables $containerEnvironmentVariables; + /** @var string[] */ + private array $dependsOn; + /** @var string[] */ + private array $secrets; + /** @var string[] */ + private array $devices; + /** @var string[] */ + private array $capAdd; + private int $shmSize; + private bool $apparmorUnconfined; + /** @var string[] */ + private array $backupVolumes; + private array $nextcloudExecCommands; + private bool $readOnlyRootFs; + private array $tmpfs; + private bool $init; + private string $imageTag; + private AioVariables $aioVariables; + private string $documentation; + private DockerActionManager $dockerActionManager; + + public function __construct( + string $identifier, + string $displayName, + string $containerName, + string $restartPolicy, + int $maxShutdownTime, + ContainerPorts $ports, + string $internalPorts, + ContainerVolumes $volumes, + ContainerEnvironmentVariables $containerEnvironmentVariables, + array $dependsOn, + array $secrets, + array $devices, + array $capAdd, + int $shmSize, + bool $apparmorUnconfined, + array $backupVolumes, + array $nextcloudExecCommands, + bool $readOnlyRootFs, + array $tmpfs, + bool $init, + string $imageTag, + AioVariables $aioVariables, + string $documentation, + DockerActionManager $dockerActionManager, + ) { + $this->identifier = $identifier; + $this->displayName = $displayName; + $this->containerName = $containerName; + $this->restartPolicy = $restartPolicy; + $this->maxShutdownTime = $maxShutdownTime; + $this->ports = $ports; + $this->internalPorts = $internalPorts; + $this->volumes = $volumes; + $this->containerEnvironmentVariables = $containerEnvironmentVariables; + $this->dependsOn = $dependsOn; + $this->secrets = $secrets; + $this->devices = $devices; + $this->capAdd = $capAdd; + $this->shmSize = $shmSize; + $this->apparmorUnconfined = $apparmorUnconfined; + $this->backupVolumes = $backupVolumes; + $this->nextcloudExecCommands = $nextcloudExecCommands; + $this->readOnlyRootFs = $readOnlyRootFs; + $this->tmpfs = $tmpfs; + $this->init = $init; + $this->imageTag = $imageTag; + $this->aioVariables = $aioVariables; + $this->documentation = $documentation; + $this->dockerActionManager = $dockerActionManager; + } + + public function GetIdentifier() : string { + return $this->identifier; + } + + public function GetDisplayName() : string { + return $this->displayName; + } + + public function GetContainerName() : string { + return $this->containerName; + } + + public function GetRestartPolicy() : string { + return $this->restartPolicy; + } + + public function GetImageTag() : string { + return $this->imageTag; + } + + public function GetReadOnlySetting() : bool { + return $this->readOnlyRootFs; + } + + public function GetInit() : bool { + return $this->init; + } + + public function GetShmSize() : int { + return $this->shmSize; + } + + public function isApparmorUnconfined() : bool { + return $this->apparmorUnconfined; + } + + public function GetMaxShutdownTime() : int { + return $this->maxShutdownTime; + } + + public function GetSecrets() : array { + return $this->secrets; + } + + public function GetTmpfs() : array { + return $this->tmpfs; + } + + public function GetDevices() : array { + return $this->devices; + } + + public function GetCapAdds() : array { + return $this->capAdd; + } + + public function GetBackupVolumes() : array { + return $this->backupVolumes; + } + + public function GetPorts() : ContainerPorts { + return $this->ports; + } + + public function GetInternalPort() : string { + return $this->internalPorts; + } + + public function GetVolumes() : ContainerVolumes { + return $this->volumes; + } + + public function GetRunningState() : IContainerState { + return $this->dockerActionManager->GetContainerRunningState($this); + } + + public function GetRestartingState() : IContainerState { + return $this->dockerActionManager->GetContainerRestartingState($this); + } + + public function GetUpdateState() : IContainerState { + return $this->dockerActionManager->GetContainerUpdateState($this); + } + + public function GetStartingState() : IContainerState { + return $this->dockerActionManager->GetContainerStartingState($this); + } + + /** + * @return string[] + */ + public function GetDependsOn() : array { + return $this->dependsOn; + } + + public function GetNextcloudExecCommands() : array { + return $this->nextcloudExecCommands; + } + + public function GetEnvironmentVariables() : ContainerEnvironmentVariables { + return $this->containerEnvironmentVariables; + } + + public function GetAioVariables() : AioVariables { + return $this->aioVariables; + } + + public function GetDocumentation() : string { + return $this->documentation; + } } diff --git a/php/src/Container/ContainerEnvironmentVariables.php b/php/src/Container/ContainerEnvironmentVariables.php index 301fcdcf9b5..a0481f72ff3 100644 --- a/php/src/Container/ContainerEnvironmentVariables.php +++ b/php/src/Container/ContainerEnvironmentVariables.php @@ -3,17 +3,17 @@ namespace AIO\Container; class ContainerEnvironmentVariables { - /** @var string[] */ - private array $variables = []; + /** @var string[] */ + private array $variables = []; - public function AddVariable(string $variable) : void { - $this->variables[] = $variable; - } + public function AddVariable(string $variable) : void { + $this->variables[] = $variable; + } - /** - * @return string[] - */ - public function GetVariables() : array { - return $this->variables; - } + /** + * @return string[] + */ + public function GetVariables() : array { + return $this->variables; + } } diff --git a/php/src/Container/ContainerPort.php b/php/src/Container/ContainerPort.php index 3ecf56cd50b..82a2dc4fa48 100644 --- a/php/src/Container/ContainerPort.php +++ b/php/src/Container/ContainerPort.php @@ -3,17 +3,17 @@ namespace AIO\Container; class ContainerPort { - public string $port; - public string $ipBinding; - public string $protocol; + public string $port; + public string $ipBinding; + public string $protocol; - public function __construct( - string $port, - string $ipBinding, - string $protocol - ) { - $this->port = $port; - $this->ipBinding = $ipBinding; - $this->protocol = $protocol; - } + public function __construct( + string $port, + string $ipBinding, + string $protocol, + ) { + $this->port = $port; + $this->ipBinding = $ipBinding; + $this->protocol = $protocol; + } } diff --git a/php/src/Container/ContainerPorts.php b/php/src/Container/ContainerPorts.php index 1e88eebc15f..82bb9c190ea 100644 --- a/php/src/Container/ContainerPorts.php +++ b/php/src/Container/ContainerPorts.php @@ -3,17 +3,17 @@ namespace AIO\Container; class ContainerPorts { - /** @var ContainerPort[] */ - private array $ports = []; + /** @var ContainerPort[] */ + private array $ports = []; - public function AddPort(ContainerPort $port) : void { - $this->ports[] = $port; - } + public function AddPort(ContainerPort $port) : void { + $this->ports[] = $port; + } - /** - * @return ContainerPort[] - */ - public function GetPorts() : array { - return $this->ports; - } -} \ No newline at end of file + /** + * @return ContainerPort[] + */ + public function GetPorts() : array { + return $this->ports; + } +} diff --git a/php/src/Container/ContainerVolume.php b/php/src/Container/ContainerVolume.php index af51759103e..30577c65a52 100644 --- a/php/src/Container/ContainerVolume.php +++ b/php/src/Container/ContainerVolume.php @@ -3,17 +3,17 @@ namespace AIO\Container; class ContainerVolume { - public string $name; - public string $mountPoint; - public bool $isWritable; + public string $name; + public string $mountPoint; + public bool $isWritable; - public function __construct( - string $name, - string $mountPoint, - bool $isWritable - ) { - $this->name = $name; - $this->mountPoint = $mountPoint; - $this->isWritable = $isWritable; - } + public function __construct( + string $name, + string $mountPoint, + bool $isWritable, + ) { + $this->name = $name; + $this->mountPoint = $mountPoint; + $this->isWritable = $isWritable; + } } diff --git a/php/src/Container/ContainerVolumes.php b/php/src/Container/ContainerVolumes.php index 23618df424d..3b2e10ae1eb 100644 --- a/php/src/Container/ContainerVolumes.php +++ b/php/src/Container/ContainerVolumes.php @@ -3,17 +3,17 @@ namespace AIO\Container; class ContainerVolumes { - /** @var ContainerVolume[] */ - private array $volumes = []; + /** @var ContainerVolume[] */ + private array $volumes = []; - public function AddVolume(ContainerVolume $volume) : void { - $this->volumes[] = $volume; - } + public function AddVolume(ContainerVolume $volume) : void { + $this->volumes[] = $volume; + } - /** - * @return ContainerVolume[] - */ - public function GetVolumes() : array { - return $this->volumes; - } + /** + * @return ContainerVolume[] + */ + public function GetVolumes() : array { + return $this->volumes; + } } diff --git a/php/src/Container/State/IContainerState.php b/php/src/Container/State/IContainerState.php index d93dab65cba..a4cfdee6a79 100644 --- a/php/src/Container/State/IContainerState.php +++ b/php/src/Container/State/IContainerState.php @@ -2,4 +2,5 @@ namespace AIO\Container\State; -interface IContainerState {} +interface IContainerState { +} diff --git a/php/src/Container/State/ImageDoesNotExistState.php b/php/src/Container/State/ImageDoesNotExistState.php index dbef10f4550..3dcb087dccb 100644 --- a/php/src/Container/State/ImageDoesNotExistState.php +++ b/php/src/Container/State/ImageDoesNotExistState.php @@ -2,5 +2,5 @@ namespace AIO\Container\State; -class ImageDoesNotExistState implements IContainerState -{} \ No newline at end of file +class ImageDoesNotExistState implements IContainerState { +} diff --git a/php/src/Container/State/NotRestartingState.php b/php/src/Container/State/NotRestartingState.php index b035377c33d..df3484bc74a 100644 --- a/php/src/Container/State/NotRestartingState.php +++ b/php/src/Container/State/NotRestartingState.php @@ -2,5 +2,5 @@ namespace AIO\Container\State; -class NotRestartingState implements IContainerState -{} \ No newline at end of file +class NotRestartingState implements IContainerState { +} diff --git a/php/src/Container/State/RestartingState.php b/php/src/Container/State/RestartingState.php index 9e94637eec3..aa089152e66 100644 --- a/php/src/Container/State/RestartingState.php +++ b/php/src/Container/State/RestartingState.php @@ -2,5 +2,5 @@ namespace AIO\Container\State; -class RestartingState implements IContainerState -{} \ No newline at end of file +class RestartingState implements IContainerState { +} diff --git a/php/src/Container/State/RunningState.php b/php/src/Container/State/RunningState.php index d5cc1915429..68b744dbca2 100644 --- a/php/src/Container/State/RunningState.php +++ b/php/src/Container/State/RunningState.php @@ -2,5 +2,5 @@ namespace AIO\Container\State; -class RunningState implements IContainerState -{} \ No newline at end of file +class RunningState implements IContainerState { +} diff --git a/php/src/Container/State/StartingState.php b/php/src/Container/State/StartingState.php index b2e357251b4..402033c182c 100644 --- a/php/src/Container/State/StartingState.php +++ b/php/src/Container/State/StartingState.php @@ -2,5 +2,5 @@ namespace AIO\Container\State; -class StartingState implements IContainerState -{} \ No newline at end of file +class StartingState implements IContainerState { +} diff --git a/php/src/Container/State/StoppedState.php b/php/src/Container/State/StoppedState.php index cbc6288541d..8ec31a4100f 100644 --- a/php/src/Container/State/StoppedState.php +++ b/php/src/Container/State/StoppedState.php @@ -2,5 +2,5 @@ namespace AIO\Container\State; -class StoppedState implements IContainerState -{} \ No newline at end of file +class StoppedState implements IContainerState { +} diff --git a/php/src/Container/State/VersionDifferentState.php b/php/src/Container/State/VersionDifferentState.php index 96ac6bdb716..01656fc7561 100644 --- a/php/src/Container/State/VersionDifferentState.php +++ b/php/src/Container/State/VersionDifferentState.php @@ -2,5 +2,5 @@ namespace AIO\Container\State; -class VersionDifferentState implements IContainerState -{} \ No newline at end of file +class VersionDifferentState implements IContainerState { +} diff --git a/php/src/Container/State/VersionEqualState.php b/php/src/Container/State/VersionEqualState.php index a4b221f18e9..c4309798342 100644 --- a/php/src/Container/State/VersionEqualState.php +++ b/php/src/Container/State/VersionEqualState.php @@ -2,5 +2,5 @@ namespace AIO\Container\State; -class VersionEqualState implements IContainerState -{} \ No newline at end of file +class VersionEqualState implements IContainerState { +} diff --git a/php/src/ContainerDefinitionFetcher.php b/php/src/ContainerDefinitionFetcher.php index 1cd4b17cd0e..2162ade680b 100644 --- a/php/src/ContainerDefinitionFetcher.php +++ b/php/src/ContainerDefinitionFetcher.php @@ -9,337 +9,331 @@ use AIO\Container\ContainerPorts; use AIO\Container\ContainerVolume; use AIO\Container\ContainerVolumes; -use AIO\Container\State\RunningState; use AIO\Data\ConfigurationManager; use AIO\Data\DataConst; use AIO\Docker\DockerActionManager; -class ContainerDefinitionFetcher -{ - private ConfigurationManager $configurationManager; - private \DI\Container $container; - - public function __construct( - ConfigurationManager $configurationManager, - \DI\Container $container - ) - { - $this->configurationManager = $configurationManager; - $this->container = $container; - } - - public function GetContainerById(string $id): Container - { - $containers = $this->FetchDefinition(); - - foreach ($containers as $container) { - if ($container->GetIdentifier() === $id) { - return $container; - } - } - - throw new \Exception("The provided id " . $id . " was not found in the container definition."); - } - - /** - * @return array - */ - private function GetDefinition(): array - { - $data = json_decode(file_get_contents(__DIR__ . '/../containers.json'), true); - - $additionalContainerNames = []; - foreach ($this->configurationManager->GetEnabledCommunityContainers() as $communityContainer) { - if ($communityContainer !== '') { - $path = DataConst::GetCommunityContainersDirectory() . '/' . $communityContainer . '/' . $communityContainer . '.json'; - $additionalData = json_decode(file_get_contents($path), true); - $data = array_merge_recursive($data, $additionalData); - if (isset($additionalData['aio_services_v1'][0]['display_name']) && $additionalData['aio_services_v1'][0]['display_name'] !== '') { - // Store container_name of community containers in variable for later - $additionalContainerNames[] = $additionalData['aio_services_v1'][0]['container_name']; - } - } - } - - $containers = []; - foreach ($data['aio_services_v1'] as $entry) { - if ($entry['container_name'] === 'nextcloud-aio-clamav') { - if (!$this->configurationManager->isClamavEnabled()) { - continue; - } - } elseif ($entry['container_name'] === 'nextcloud-aio-onlyoffice') { - if (!$this->configurationManager->isOnlyofficeEnabled()) { - continue; - } - } elseif ($entry['container_name'] === 'nextcloud-aio-collabora') { - if (!$this->configurationManager->isCollaboraEnabled()) { - continue; - } - } elseif ($entry['container_name'] === 'nextcloud-aio-talk') { - if (!$this->configurationManager->isTalkEnabled()) { - continue; - } - } elseif ($entry['container_name'] === 'nextcloud-aio-talk-recording') { - if (!$this->configurationManager->isTalkRecordingEnabled()) { - continue; - } - } elseif ($entry['container_name'] === 'nextcloud-aio-imaginary') { - if (!$this->configurationManager->isImaginaryEnabled()) { - continue; - } - } elseif ($entry['container_name'] === 'nextcloud-aio-fulltextsearch') { - if (!$this->configurationManager->isFulltextsearchEnabled()) { - continue; - } - } elseif ($entry['container_name'] === 'nextcloud-aio-docker-socket-proxy') { - if (!$this->configurationManager->isDockerSocketProxyEnabled()) { - continue; - } - } elseif ($entry['container_name'] === 'nextcloud-aio-whiteboard') { - if (!$this->configurationManager->isWhiteboardEnabled()) { - continue; - } - } - - $ports = new ContainerPorts(); - if (isset($entry['ports'])) { - foreach ($entry['ports'] as $value) { - $ports->AddPort( - new ContainerPort( - $value['port_number'], - $value['ip_binding'], - $value['protocol'] - ) - ); - } - } - - $volumes = new ContainerVolumes(); - if (isset($entry['volumes'])) { - foreach ($entry['volumes'] as $value) { - if($value['source'] === '%BORGBACKUP_HOST_LOCATION%') { - $value['source'] = $this->configurationManager->GetBorgBackupHostLocation(); - if($value['source'] === '') { - continue; - } - } - if($value['source'] === '%NEXTCLOUD_MOUNT%') { - $value['source'] = $this->configurationManager->GetNextcloudMount(); - if($value['source'] === '') { - continue; - } - } elseif ($value['source'] === '%NEXTCLOUD_DATADIR%') { - $value['source'] = $this->configurationManager->GetNextcloudDatadirMount(); - if ($value['source'] === '') { - continue; - } - } elseif ($value['source'] === '%WATCHTOWER_DOCKER_SOCKET_PATH%') { - $value['source'] = $this->configurationManager->GetDockerSocketPath(); - if($value['source'] === '') { - continue; - } - } elseif ($value['source'] === '%NEXTCLOUD_TRUSTED_CACERTS_DIR%') { - $value['source'] = $this->configurationManager->GetTrustedCacertsDir(); - if($value['source'] === '') { - continue; - } - } - if ($value['destination'] === '%NEXTCLOUD_MOUNT%') { - $value['destination'] = $this->configurationManager->GetNextcloudMount(); - if($value['destination'] === '') { - continue; - } - } - $volumes->AddVolume( - new ContainerVolume( - $value['source'], - $value['destination'], - $value['writeable'] - ) - ); - } - } - - $dependsOn = []; - if (isset($entry['depends_on'])) { - $valueDependsOn = $entry['depends_on']; - if ($entry['container_name'] === 'nextcloud-aio-apache') { - // Add community containers first and default ones last so that aio_variables works correctly - $valueDependsOnTemp = []; - foreach ($additionalContainerNames as $containerName) { - $valueDependsOnTemp[] = $containerName; - } - $valueDependsOn = array_merge_recursive($valueDependsOnTemp, $valueDependsOn); - } - foreach ($valueDependsOn as $value) { - if ($value === 'nextcloud-aio-clamav') { - if (!$this->configurationManager->isClamavEnabled()) { - continue; - } - } elseif ($value === 'nextcloud-aio-onlyoffice') { - if (!$this->configurationManager->isOnlyofficeEnabled()) { - continue; - } - } elseif ($value === 'nextcloud-aio-collabora') { - if (!$this->configurationManager->isCollaboraEnabled()) { - continue; - } - } elseif ($value === 'nextcloud-aio-talk') { - if (!$this->configurationManager->isTalkEnabled()) { - continue; - } - } elseif ($value === 'nextcloud-aio-talk-recording') { - if (!$this->configurationManager->isTalkRecordingEnabled()) { - continue; - } - } elseif ($value === 'nextcloud-aio-imaginary') { - if (!$this->configurationManager->isImaginaryEnabled()) { - continue; - } - } elseif ($value === 'nextcloud-aio-fulltextsearch') { - if (!$this->configurationManager->isFulltextsearchEnabled()) { - continue; - } - } elseif ($value === 'nextcloud-aio-docker-socket-proxy') { - if (!$this->configurationManager->isDockerSocketProxyEnabled()) { - continue; - } - } elseif ($value === 'nextcloud-aio-whiteboard') { - if (!$this->configurationManager->isWhiteboardEnabled()) { - continue; - } - } - $dependsOn[] = $value; - } - } - - $variables = new ContainerEnvironmentVariables(); - if (isset($entry['environment'])) { - foreach ($entry['environment'] as $value) { - $variables->AddVariable($value); - } - } - - $aioVariables = new AioVariables(); - if (isset($entry['aio_variables'])) { - foreach ($entry['aio_variables'] as $value) { - $aioVariables->AddVariable($value); - } - } - - $displayName = ''; - if (isset($entry['display_name'])) { - $displayName = $entry['display_name']; - } - - $restartPolicy = ''; - if (isset($entry['restart'])) { - $restartPolicy = $entry['restart']; - } - - $maxShutdownTime = 10; - if (isset($entry['stop_grace_period'])) { - $maxShutdownTime = $entry['stop_grace_period']; - } - - $internalPort = ''; - if (isset($entry['internal_port'])) { - $internalPort = $entry['internal_port']; - } - - $secrets = []; - if (isset($entry['secrets'])) { - $secrets = $entry['secrets']; - } - - $devices = []; - if (isset($entry['devices'])) { - $devices = $entry['devices']; - } - - $capAdd = []; - if (isset($entry['cap_add'])) { - $capAdd = $entry['cap_add']; - } - - $shmSize = -1; - if (isset($entry['shm_size'])) { - $shmSize = $entry['shm_size']; - } - - $apparmorUnconfined = false; - if (isset($entry['apparmor_unconfined'])) { - $apparmorUnconfined = $entry['apparmor_unconfined']; - } - - $backupVolumes = []; - if (isset($entry['backup_volumes'])) { - $backupVolumes = $entry['backup_volumes']; - } - - $nextcloudExecCommands = []; - if (isset($entry['nextcloud_exec_commands'])) { - $nextcloudExecCommands = $entry['nextcloud_exec_commands']; - } - - $readOnlyRootFs = false; - if (isset($entry['read_only'])) { - $readOnlyRootFs = $entry['read_only']; - } - - $tmpfs = []; - if (isset($entry['tmpfs'])) { - $tmpfs = $entry['tmpfs']; - } - - $init = true; - if (isset($entry['init'])) { - $init = $entry['init']; - } - - $imageTag = '%AIO_CHANNEL%'; - if (isset($entry['image_tag'])) { - $imageTag = $entry['image_tag']; - } - - $documentation = ''; - if (isset($entry['documentation'])) { - $documentation = $entry['documentation']; - } - - $containers[] = new Container( - $entry['container_name'], - $displayName, - $entry['image'], - $restartPolicy, - $maxShutdownTime, - $ports, - $internalPort, - $volumes, - $variables, - $dependsOn, - $secrets, - $devices, - $capAdd, - $shmSize, - $apparmorUnconfined, - $backupVolumes, - $nextcloudExecCommands, - $readOnlyRootFs, - $tmpfs, - $init, - $imageTag, - $aioVariables, - $documentation, - $this->container->get(DockerActionManager::class) - ); - } - - return $containers; - } - - public function FetchDefinition(): array - { - return $this->GetDefinition(); - } +class ContainerDefinitionFetcher { + private ConfigurationManager $configurationManager; + private \DI\Container $container; + + public function __construct( + ConfigurationManager $configurationManager, + \DI\Container $container, + ) { + $this->configurationManager = $configurationManager; + $this->container = $container; + } + + public function GetContainerById(string $id): Container { + $containers = $this->FetchDefinition(); + + foreach ($containers as $container) { + if ($container->GetIdentifier() === $id) { + return $container; + } + } + + throw new \Exception('The provided id ' . $id . ' was not found in the container definition.'); + } + + /** + * @return array + */ + private function GetDefinition(): array { + $data = json_decode(file_get_contents(__DIR__ . '/../containers.json'), true); + + $additionalContainerNames = []; + foreach ($this->configurationManager->GetEnabledCommunityContainers() as $communityContainer) { + if ($communityContainer !== '') { + $path = DataConst::GetCommunityContainersDirectory() . '/' . $communityContainer . '/' . $communityContainer . '.json'; + $additionalData = json_decode(file_get_contents($path), true); + $data = array_merge_recursive($data, $additionalData); + if (isset($additionalData['aio_services_v1'][0]['display_name']) && $additionalData['aio_services_v1'][0]['display_name'] !== '') { + // Store container_name of community containers in variable for later + $additionalContainerNames[] = $additionalData['aio_services_v1'][0]['container_name']; + } + } + } + + $containers = []; + foreach ($data['aio_services_v1'] as $entry) { + if ($entry['container_name'] === 'nextcloud-aio-clamav') { + if (!$this->configurationManager->isClamavEnabled()) { + continue; + } + } elseif ($entry['container_name'] === 'nextcloud-aio-onlyoffice') { + if (!$this->configurationManager->isOnlyofficeEnabled()) { + continue; + } + } elseif ($entry['container_name'] === 'nextcloud-aio-collabora') { + if (!$this->configurationManager->isCollaboraEnabled()) { + continue; + } + } elseif ($entry['container_name'] === 'nextcloud-aio-talk') { + if (!$this->configurationManager->isTalkEnabled()) { + continue; + } + } elseif ($entry['container_name'] === 'nextcloud-aio-talk-recording') { + if (!$this->configurationManager->isTalkRecordingEnabled()) { + continue; + } + } elseif ($entry['container_name'] === 'nextcloud-aio-imaginary') { + if (!$this->configurationManager->isImaginaryEnabled()) { + continue; + } + } elseif ($entry['container_name'] === 'nextcloud-aio-fulltextsearch') { + if (!$this->configurationManager->isFulltextsearchEnabled()) { + continue; + } + } elseif ($entry['container_name'] === 'nextcloud-aio-docker-socket-proxy') { + if (!$this->configurationManager->isDockerSocketProxyEnabled()) { + continue; + } + } elseif ($entry['container_name'] === 'nextcloud-aio-whiteboard') { + if (!$this->configurationManager->isWhiteboardEnabled()) { + continue; + } + } + + $ports = new ContainerPorts(); + if (isset($entry['ports'])) { + foreach ($entry['ports'] as $value) { + $ports->AddPort( + new ContainerPort( + $value['port_number'], + $value['ip_binding'], + $value['protocol'] + ) + ); + } + } + + $volumes = new ContainerVolumes(); + if (isset($entry['volumes'])) { + foreach ($entry['volumes'] as $value) { + if ($value['source'] === '%BORGBACKUP_HOST_LOCATION%') { + $value['source'] = $this->configurationManager->GetBorgBackupHostLocation(); + if ($value['source'] === '') { + continue; + } + } + if ($value['source'] === '%NEXTCLOUD_MOUNT%') { + $value['source'] = $this->configurationManager->GetNextcloudMount(); + if ($value['source'] === '') { + continue; + } + } elseif ($value['source'] === '%NEXTCLOUD_DATADIR%') { + $value['source'] = $this->configurationManager->GetNextcloudDatadirMount(); + if ($value['source'] === '') { + continue; + } + } elseif ($value['source'] === '%WATCHTOWER_DOCKER_SOCKET_PATH%') { + $value['source'] = $this->configurationManager->GetDockerSocketPath(); + if ($value['source'] === '') { + continue; + } + } elseif ($value['source'] === '%NEXTCLOUD_TRUSTED_CACERTS_DIR%') { + $value['source'] = $this->configurationManager->GetTrustedCacertsDir(); + if ($value['source'] === '') { + continue; + } + } + if ($value['destination'] === '%NEXTCLOUD_MOUNT%') { + $value['destination'] = $this->configurationManager->GetNextcloudMount(); + if ($value['destination'] === '') { + continue; + } + } + $volumes->AddVolume( + new ContainerVolume( + $value['source'], + $value['destination'], + $value['writeable'] + ) + ); + } + } + + $dependsOn = []; + if (isset($entry['depends_on'])) { + $valueDependsOn = $entry['depends_on']; + if ($entry['container_name'] === 'nextcloud-aio-apache') { + // Add community containers first and default ones last so that aio_variables works correctly + $valueDependsOnTemp = []; + foreach ($additionalContainerNames as $containerName) { + $valueDependsOnTemp[] = $containerName; + } + $valueDependsOn = array_merge_recursive($valueDependsOnTemp, $valueDependsOn); + } + foreach ($valueDependsOn as $value) { + if ($value === 'nextcloud-aio-clamav') { + if (!$this->configurationManager->isClamavEnabled()) { + continue; + } + } elseif ($value === 'nextcloud-aio-onlyoffice') { + if (!$this->configurationManager->isOnlyofficeEnabled()) { + continue; + } + } elseif ($value === 'nextcloud-aio-collabora') { + if (!$this->configurationManager->isCollaboraEnabled()) { + continue; + } + } elseif ($value === 'nextcloud-aio-talk') { + if (!$this->configurationManager->isTalkEnabled()) { + continue; + } + } elseif ($value === 'nextcloud-aio-talk-recording') { + if (!$this->configurationManager->isTalkRecordingEnabled()) { + continue; + } + } elseif ($value === 'nextcloud-aio-imaginary') { + if (!$this->configurationManager->isImaginaryEnabled()) { + continue; + } + } elseif ($value === 'nextcloud-aio-fulltextsearch') { + if (!$this->configurationManager->isFulltextsearchEnabled()) { + continue; + } + } elseif ($value === 'nextcloud-aio-docker-socket-proxy') { + if (!$this->configurationManager->isDockerSocketProxyEnabled()) { + continue; + } + } elseif ($value === 'nextcloud-aio-whiteboard') { + if (!$this->configurationManager->isWhiteboardEnabled()) { + continue; + } + } + $dependsOn[] = $value; + } + } + + $variables = new ContainerEnvironmentVariables(); + if (isset($entry['environment'])) { + foreach ($entry['environment'] as $value) { + $variables->AddVariable($value); + } + } + + $aioVariables = new AioVariables(); + if (isset($entry['aio_variables'])) { + foreach ($entry['aio_variables'] as $value) { + $aioVariables->AddVariable($value); + } + } + + $displayName = ''; + if (isset($entry['display_name'])) { + $displayName = $entry['display_name']; + } + + $restartPolicy = ''; + if (isset($entry['restart'])) { + $restartPolicy = $entry['restart']; + } + + $maxShutdownTime = 10; + if (isset($entry['stop_grace_period'])) { + $maxShutdownTime = $entry['stop_grace_period']; + } + + $internalPort = ''; + if (isset($entry['internal_port'])) { + $internalPort = $entry['internal_port']; + } + + $secrets = []; + if (isset($entry['secrets'])) { + $secrets = $entry['secrets']; + } + + $devices = []; + if (isset($entry['devices'])) { + $devices = $entry['devices']; + } + + $capAdd = []; + if (isset($entry['cap_add'])) { + $capAdd = $entry['cap_add']; + } + + $shmSize = -1; + if (isset($entry['shm_size'])) { + $shmSize = $entry['shm_size']; + } + + $apparmorUnconfined = false; + if (isset($entry['apparmor_unconfined'])) { + $apparmorUnconfined = $entry['apparmor_unconfined']; + } + + $backupVolumes = []; + if (isset($entry['backup_volumes'])) { + $backupVolumes = $entry['backup_volumes']; + } + + $nextcloudExecCommands = []; + if (isset($entry['nextcloud_exec_commands'])) { + $nextcloudExecCommands = $entry['nextcloud_exec_commands']; + } + + $readOnlyRootFs = false; + if (isset($entry['read_only'])) { + $readOnlyRootFs = $entry['read_only']; + } + + $tmpfs = []; + if (isset($entry['tmpfs'])) { + $tmpfs = $entry['tmpfs']; + } + + $init = true; + if (isset($entry['init'])) { + $init = $entry['init']; + } + + $imageTag = '%AIO_CHANNEL%'; + if (isset($entry['image_tag'])) { + $imageTag = $entry['image_tag']; + } + + $documentation = ''; + if (isset($entry['documentation'])) { + $documentation = $entry['documentation']; + } + + $containers[] = new Container( + $entry['container_name'], + $displayName, + $entry['image'], + $restartPolicy, + $maxShutdownTime, + $ports, + $internalPort, + $volumes, + $variables, + $dependsOn, + $secrets, + $devices, + $capAdd, + $shmSize, + $apparmorUnconfined, + $backupVolumes, + $nextcloudExecCommands, + $readOnlyRootFs, + $tmpfs, + $init, + $imageTag, + $aioVariables, + $documentation, + $this->container->get(DockerActionManager::class) + ); + } + + return $containers; + } + + public function FetchDefinition(): array { + return $this->GetDefinition(); + } } diff --git a/php/src/Controller/ConfigurationController.php b/php/src/Controller/ConfigurationController.php index 835d7b6294f..9d067ef2ee7 100644 --- a/php/src/Controller/ConfigurationController.php +++ b/php/src/Controller/ConfigurationController.php @@ -2,148 +2,145 @@ namespace AIO\Controller; -use AIO\ContainerDefinitionFetcher; use AIO\Data\ConfigurationManager; use AIO\Data\InvalidSettingConfigurationException; -use AIO\Docker\DockerActionManager; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; -class ConfigurationController -{ - private ConfigurationManager $configurationManager; - - public function __construct( - ConfigurationManager $configurationManager - ) { - $this->configurationManager = $configurationManager; - } - - public function SetConfig(Request $request, Response $response, array $args) : Response { - try { - if (isset($request->getParsedBody()['domain'])) { - $domain = $request->getParsedBody()['domain'] ?? ''; - $this->configurationManager->SetDomain($domain); - } - - if (isset($request->getParsedBody()['current-master-password']) || isset($request->getParsedBody()['new-master-password'])) { - $currentMasterPassword = $request->getParsedBody()['current-master-password'] ?? ''; - $newMasterPassword = $request->getParsedBody()['new-master-password'] ?? ''; - $this->configurationManager->ChangeMasterPassword($currentMasterPassword, $newMasterPassword); - } - - if (isset($request->getParsedBody()['borg_backup_host_location'])) { - $location = $request->getParsedBody()['borg_backup_host_location'] ?? ''; - $this->configurationManager->SetBorgBackupHostLocation($location); - } - - if (isset($request->getParsedBody()['borg_restore_host_location']) || isset($request->getParsedBody()['borg_restore_password'])) { - $restoreLocation = $request->getParsedBody()['borg_restore_host_location'] ?? ''; - $borgPassword = $request->getParsedBody()['borg_restore_password'] ?? ''; - $this->configurationManager->SetBorgRestoreHostLocationAndPassword($restoreLocation, $borgPassword); - } - - if (isset($request->getParsedBody()['daily_backup_time'])) { - if (isset($request->getParsedBody()['automatic_updates'])) { - $enableAutomaticUpdates = true; - } else { - $enableAutomaticUpdates = false; - } - if (isset($request->getParsedBody()['success_notification'])) { - $successNotification = true; - } else { - $successNotification = false; - } - $dailyBackupTime = $request->getParsedBody()['daily_backup_time'] ?? ''; - $this->configurationManager->SetDailyBackupTime($dailyBackupTime, $enableAutomaticUpdates, $successNotification); - } - - if (isset($request->getParsedBody()['delete_daily_backup_time'])) { - $this->configurationManager->DeleteDailyBackupTime(); - } - - if (isset($request->getParsedBody()['additional_backup_directories'])) { - $additionalBackupDirectories = $request->getParsedBody()['additional_backup_directories'] ?? ''; - $this->configurationManager->SetAdditionalBackupDirectories($additionalBackupDirectories); - } - - if (isset($request->getParsedBody()['delete_timezone'])) { - $this->configurationManager->DeleteTimezone(); - } - - if (isset($request->getParsedBody()['timezone'])) { - $timezone = $request->getParsedBody()['timezone'] ?? ''; - $this->configurationManager->SetTimezone($timezone); - } - - if (isset($request->getParsedBody()['options-form'])) { - if (isset($request->getParsedBody()['collabora']) && isset($request->getParsedBody()['onlyoffice'])) { - throw new InvalidSettingConfigurationException("Collabora and Onlyoffice are not allowed to be enabled at the same time!"); - } - if (isset($request->getParsedBody()['clamav'])) { - $this->configurationManager->SetClamavEnabledState(1); - } else { - $this->configurationManager->SetClamavEnabledState(0); - } - if (isset($request->getParsedBody()['onlyoffice'])) { - $this->configurationManager->SetOnlyofficeEnabledState(1); - } else { - $this->configurationManager->SetOnlyofficeEnabledState(0); - } - if (isset($request->getParsedBody()['collabora'])) { - $this->configurationManager->SetCollaboraEnabledState(1); - } else { - $this->configurationManager->SetCollaboraEnabledState(0); - } - if (isset($request->getParsedBody()['talk'])) { - $this->configurationManager->SetTalkEnabledState(1); - } else { - $this->configurationManager->SetTalkEnabledState(0); - } - if (isset($request->getParsedBody()['talk-recording'])) { - $this->configurationManager->SetTalkRecordingEnabledState(1); - } else { - $this->configurationManager->SetTalkRecordingEnabledState(0); - } - if (isset($request->getParsedBody()['imaginary'])) { - $this->configurationManager->SetImaginaryEnabledState(1); - } else { - $this->configurationManager->SetImaginaryEnabledState(0); - } - if (isset($request->getParsedBody()['fulltextsearch'])) { - $this->configurationManager->SetFulltextsearchEnabledState(1); - } else { - $this->configurationManager->SetFulltextsearchEnabledState(0); - } - if (isset($request->getParsedBody()['docker-socket-proxy'])) { - $this->configurationManager->SetDockerSocketProxyEnabledState(1); - } else { - $this->configurationManager->SetDockerSocketProxyEnabledState(0); - } - if (isset($request->getParsedBody()['whiteboard'])) { - $this->configurationManager->SetWhiteboardEnabledState(1); - } else { - $this->configurationManager->SetWhiteboardEnabledState(0); - } - } - - if (isset($request->getParsedBody()['delete_collabora_dictionaries'])) { - $this->configurationManager->DeleteCollaboraDictionaries(); - } - - if (isset($request->getParsedBody()['collabora_dictionaries'])) { - $collaboraDictionaries = $request->getParsedBody()['collabora_dictionaries'] ?? ''; - $this->configurationManager->SetCollaboraDictionaries($collaboraDictionaries); - } - - if (isset($request->getParsedBody()['delete_borg_backup_host_location'])) { - $this->configurationManager->DeleteBorgBackupHostLocation(); - } - - return $response->withStatus(201)->withHeader('Location', '/'); - } catch (InvalidSettingConfigurationException $ex) { - $response->getBody()->write($ex->getMessage()); - return $response->withStatus(422); - } - } +class ConfigurationController { + private ConfigurationManager $configurationManager; + + public function __construct( + ConfigurationManager $configurationManager, + ) { + $this->configurationManager = $configurationManager; + } + + public function SetConfig(Request $request, Response $response, array $args) : Response { + try { + if (isset($request->getParsedBody()['domain'])) { + $domain = $request->getParsedBody()['domain'] ?? ''; + $this->configurationManager->SetDomain($domain); + } + + if (isset($request->getParsedBody()['current-master-password']) || isset($request->getParsedBody()['new-master-password'])) { + $currentMasterPassword = $request->getParsedBody()['current-master-password'] ?? ''; + $newMasterPassword = $request->getParsedBody()['new-master-password'] ?? ''; + $this->configurationManager->ChangeMasterPassword($currentMasterPassword, $newMasterPassword); + } + + if (isset($request->getParsedBody()['borg_backup_host_location'])) { + $location = $request->getParsedBody()['borg_backup_host_location'] ?? ''; + $this->configurationManager->SetBorgBackupHostLocation($location); + } + + if (isset($request->getParsedBody()['borg_restore_host_location']) || isset($request->getParsedBody()['borg_restore_password'])) { + $restoreLocation = $request->getParsedBody()['borg_restore_host_location'] ?? ''; + $borgPassword = $request->getParsedBody()['borg_restore_password'] ?? ''; + $this->configurationManager->SetBorgRestoreHostLocationAndPassword($restoreLocation, $borgPassword); + } + + if (isset($request->getParsedBody()['daily_backup_time'])) { + if (isset($request->getParsedBody()['automatic_updates'])) { + $enableAutomaticUpdates = true; + } else { + $enableAutomaticUpdates = false; + } + if (isset($request->getParsedBody()['success_notification'])) { + $successNotification = true; + } else { + $successNotification = false; + } + $dailyBackupTime = $request->getParsedBody()['daily_backup_time'] ?? ''; + $this->configurationManager->SetDailyBackupTime($dailyBackupTime, $enableAutomaticUpdates, $successNotification); + } + + if (isset($request->getParsedBody()['delete_daily_backup_time'])) { + $this->configurationManager->DeleteDailyBackupTime(); + } + + if (isset($request->getParsedBody()['additional_backup_directories'])) { + $additionalBackupDirectories = $request->getParsedBody()['additional_backup_directories'] ?? ''; + $this->configurationManager->SetAdditionalBackupDirectories($additionalBackupDirectories); + } + + if (isset($request->getParsedBody()['delete_timezone'])) { + $this->configurationManager->DeleteTimezone(); + } + + if (isset($request->getParsedBody()['timezone'])) { + $timezone = $request->getParsedBody()['timezone'] ?? ''; + $this->configurationManager->SetTimezone($timezone); + } + + if (isset($request->getParsedBody()['options-form'])) { + if (isset($request->getParsedBody()['collabora']) && isset($request->getParsedBody()['onlyoffice'])) { + throw new InvalidSettingConfigurationException('Collabora and Onlyoffice are not allowed to be enabled at the same time!'); + } + if (isset($request->getParsedBody()['clamav'])) { + $this->configurationManager->SetClamavEnabledState(1); + } else { + $this->configurationManager->SetClamavEnabledState(0); + } + if (isset($request->getParsedBody()['onlyoffice'])) { + $this->configurationManager->SetOnlyofficeEnabledState(1); + } else { + $this->configurationManager->SetOnlyofficeEnabledState(0); + } + if (isset($request->getParsedBody()['collabora'])) { + $this->configurationManager->SetCollaboraEnabledState(1); + } else { + $this->configurationManager->SetCollaboraEnabledState(0); + } + if (isset($request->getParsedBody()['talk'])) { + $this->configurationManager->SetTalkEnabledState(1); + } else { + $this->configurationManager->SetTalkEnabledState(0); + } + if (isset($request->getParsedBody()['talk-recording'])) { + $this->configurationManager->SetTalkRecordingEnabledState(1); + } else { + $this->configurationManager->SetTalkRecordingEnabledState(0); + } + if (isset($request->getParsedBody()['imaginary'])) { + $this->configurationManager->SetImaginaryEnabledState(1); + } else { + $this->configurationManager->SetImaginaryEnabledState(0); + } + if (isset($request->getParsedBody()['fulltextsearch'])) { + $this->configurationManager->SetFulltextsearchEnabledState(1); + } else { + $this->configurationManager->SetFulltextsearchEnabledState(0); + } + if (isset($request->getParsedBody()['docker-socket-proxy'])) { + $this->configurationManager->SetDockerSocketProxyEnabledState(1); + } else { + $this->configurationManager->SetDockerSocketProxyEnabledState(0); + } + if (isset($request->getParsedBody()['whiteboard'])) { + $this->configurationManager->SetWhiteboardEnabledState(1); + } else { + $this->configurationManager->SetWhiteboardEnabledState(0); + } + } + + if (isset($request->getParsedBody()['delete_collabora_dictionaries'])) { + $this->configurationManager->DeleteCollaboraDictionaries(); + } + + if (isset($request->getParsedBody()['collabora_dictionaries'])) { + $collaboraDictionaries = $request->getParsedBody()['collabora_dictionaries'] ?? ''; + $this->configurationManager->SetCollaboraDictionaries($collaboraDictionaries); + } + + if (isset($request->getParsedBody()['delete_borg_backup_host_location'])) { + $this->configurationManager->DeleteBorgBackupHostLocation(); + } + + return $response->withStatus(201)->withHeader('Location', '/'); + } catch (InvalidSettingConfigurationException $ex) { + $response->getBody()->write($ex->getMessage()); + return $response->withStatus(422); + } + } } diff --git a/php/src/Controller/DockerController.php b/php/src/Controller/DockerController.php index 2d5367386dd..078f0180215 100644 --- a/php/src/Controller/DockerController.php +++ b/php/src/Controller/DockerController.php @@ -4,288 +4,281 @@ use AIO\Container\State\RunningState; use AIO\ContainerDefinitionFetcher; +use AIO\Data\ConfigurationManager; use AIO\Docker\DockerActionManager; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; -use AIO\Data\ConfigurationManager; -class DockerController -{ - private DockerActionManager $dockerActionManager; - private ContainerDefinitionFetcher $containerDefinitionFetcher; - private const string TOP_CONTAINER = 'nextcloud-aio-apache'; - private ConfigurationManager $configurationManager; - - public function __construct( - DockerActionManager $dockerActionManager, - ContainerDefinitionFetcher $containerDefinitionFetcher, - ConfigurationManager $configurationManager - ) { - $this->dockerActionManager = $dockerActionManager; - $this->containerDefinitionFetcher = $containerDefinitionFetcher; - $this->configurationManager = $configurationManager; - } - - private function PerformRecursiveContainerStart(string $id, bool $pullImage = true) : void { - $container = $this->containerDefinitionFetcher->GetContainerById($id); - - foreach($container->GetDependsOn() as $dependency) { - $this->PerformRecursiveContainerStart($dependency, $pullImage); - } - - // Don't start if container is already running - // This is expected to happen if a container is defined in depends_on of multiple containers - if ($container->GetRunningState() instanceof RunningState) { - error_log('Not starting ' . $id . ' because it was already started.'); - return; - } - - // Skip database image pull if the last shutdown was not clean - if ($id === 'nextcloud-aio-database') { - if ($this->dockerActionManager->GetDatabasecontainerExitCode() > 0) { - $pullImage = false; - error_log('Not pulling the latest database image because the container was not correctly shut down.'); - } - } - - // Check if docker hub is reachable in order to make sure that we do not try to pull an image if it is down - // and try to mitigate issues that are arising due to that - if ($pullImage) { - if (!$this->dockerActionManager->isDockerHubReachable($container)) { - $pullImage = false; - error_log('Not pulling the image for the ' . $container->GetContainerName() . ' container because docker hub does not seem to be reachable.'); - } - } - - $this->dockerActionManager->DeleteContainer($container); - $this->dockerActionManager->CreateVolumes($container); - if ($pullImage) { - $this->dockerActionManager->PullImage($container); - } - $this->dockerActionManager->CreateContainer($container); - $this->dockerActionManager->StartContainer($container); - $this->dockerActionManager->ConnectContainerToNetwork($container); - } - - public function GetLogs(Request $request, Response $response, array $args) : Response - { - $id = $request->getQueryParams()['id']; - if (str_starts_with($id, 'nextcloud-aio-')) { - $logs = $this->dockerActionManager->GetLogs($id); - } else { - $logs = 'Container not found.'; - } - - $body = $response->getBody(); - $body->write($logs); - - return $response - ->withStatus(200) - ->withHeader('Content-Type', 'text/plain; charset=utf-8') - ->withHeader('Content-Disposition', 'inline'); - } - - public function StartBackupContainerBackup(Request $request, Response $response, array $args) : Response { - $this->startBackup(); - return $response->withStatus(201)->withHeader('Location', '/'); - } - - public function startBackup() : void { - $config = $this->configurationManager->GetConfig(); - $config['backup-mode'] = 'backup'; - $this->configurationManager->WriteConfig($config); - - $id = self::TOP_CONTAINER; - $this->PerformRecursiveContainerStop($id); - - $id = 'nextcloud-aio-borgbackup'; - $this->PerformRecursiveContainerStart($id); - } - - public function StartBackupContainerCheck(Request $request, Response $response, array $args) : Response { - $this->checkBackup(); - return $response->withStatus(201)->withHeader('Location', '/'); - } - - public function checkBackup() : void { - $config = $this->configurationManager->GetConfig(); - $config['backup-mode'] = 'check'; - $this->configurationManager->WriteConfig($config); - - $id = 'nextcloud-aio-borgbackup'; - $this->PerformRecursiveContainerStart($id); - } - - public function StartBackupContainerRestore(Request $request, Response $response, array $args) : Response { - $config = $this->configurationManager->GetConfig(); - $config['backup-mode'] = 'restore'; - $config['selected-restore-time'] = $request->getParsedBody()['selected_restore_time'] ?? ''; - $this->configurationManager->WriteConfig($config); - - $id = self::TOP_CONTAINER; - $this->PerformRecursiveContainerStop($id); - - $id = 'nextcloud-aio-borgbackup'; - $this->PerformRecursiveContainerStart($id); - - return $response->withStatus(201)->withHeader('Location', '/'); - } - - public function StartBackupContainerCheckRepair(Request $request, Response $response, array $args) : Response { - $config = $this->configurationManager->GetConfig(); - $config['backup-mode'] = 'check-repair'; - $this->configurationManager->WriteConfig($config); - - $id = 'nextcloud-aio-borgbackup'; - $this->PerformRecursiveContainerStart($id); - - // Restore to backup check which is needed to make the UI logic work correctly - $config = $this->configurationManager->GetConfig(); - $config['backup-mode'] = 'check'; - $this->configurationManager->WriteConfig($config); - - return $response->withStatus(201)->withHeader('Location', '/'); - } - - public function StartBackupContainerTest(Request $request, Response $response, array $args) : Response { - $config = $this->configurationManager->GetConfig(); - $config['backup-mode'] = 'test'; - $config['instance_restore_attempt'] = 0; - $this->configurationManager->WriteConfig($config); - - $id = self::TOP_CONTAINER; - $this->PerformRecursiveContainerStop($id); - - $id = 'nextcloud-aio-borgbackup'; - $this->PerformRecursiveContainerStart($id); - - return $response->withStatus(201)->withHeader('Location', '/'); - } - - public function StartContainer(Request $request, Response $response, array $args) : Response - { - $uri = $request->getUri(); - $host = $uri->getHost(); - $port = $uri->getPort(); - if ($port === 8000) { - error_log('The AIO_URL-port was discovered to be 8000 which is not expected. It is now set to 443.'); - $port = 443; - } - - if (isset($request->getParsedBody()['install_latest_major'])) { - $installLatestMajor = 30; - } else { - $installLatestMajor = ""; - } - - $config = $this->configurationManager->GetConfig(); - // set AIO_URL - $config['AIO_URL'] = $host . ':' . $port; - // set wasStartButtonClicked - $config['wasStartButtonClicked'] = 1; - // set install_latest_major - $config['install_latest_major'] = $installLatestMajor; - $this->configurationManager->WriteConfig($config); - - // Start container - $this->startTopContainer(true); - - // Clear apcu cache in order to check if container updates are available - // Temporarily disabled as it leads much faster to docker rate limits - // apcu_clear_cache(); - - return $response->withStatus(201)->withHeader('Location', '/'); - } - - public function startTopContainer(bool $pullImage) : void { - $config = $this->configurationManager->GetConfig(); - // set AIO_TOKEN - $config['AIO_TOKEN'] = bin2hex(random_bytes(24)); - $this->configurationManager->WriteConfig($config); - - // Stop domaincheck since apache would not be able to start otherwise - $this->StopDomaincheckContainer(); - - $id = self::TOP_CONTAINER; - - $this->PerformRecursiveContainerStart($id, $pullImage); - } - - public function StartWatchtowerContainer(Request $request, Response $response, array $args) : Response { - $this->startWatchtower(); - return $response->withStatus(201)->withHeader('Location', '/'); - } - - public function startWatchtower() : void { - $id = 'nextcloud-aio-watchtower'; - - $this->PerformRecursiveContainerStart($id); - } - - private function PerformRecursiveContainerStop(string $id) : void - { - $container = $this->containerDefinitionFetcher->GetContainerById($id); - foreach($container->GetDependsOn() as $dependency) { - $this->PerformRecursiveContainerStop($dependency); - } - - // Disconnecting is not needed. This also allows to start the containers manually via docker-cli - //$this->dockerActionManager->DisconnectContainerFromNetwork($container); - $this->dockerActionManager->StopContainer($container); - } - - public function StopContainer(Request $request, Response $response, array $args) : Response - { - $id = self::TOP_CONTAINER; - $this->PerformRecursiveContainerStop($id); - - return $response->withStatus(201)->withHeader('Location', '/'); - } - - public function stopTopContainer() : void { - $id = self::TOP_CONTAINER; - $this->PerformRecursiveContainerStop($id); - } - - public function StartDomaincheckContainer() : void - { - # Don't start if domain is already set - if ($this->configurationManager->GetDomain() !== '' || $this->configurationManager->wasStartButtonClicked()) { - return; - } - - $id = 'nextcloud-aio-domaincheck'; - - $cacheKey = 'domaincheckWasStarted'; - - $domaincheckContainer = $this->containerDefinitionFetcher->GetContainerById($id); - $apacheContainer = $this->containerDefinitionFetcher->GetContainerById(self::TOP_CONTAINER); - // Don't start if apache is already running - if ($apacheContainer->GetRunningState() instanceof RunningState) { - return; - // Don't start if domaincheck is already running - } elseif ($domaincheckContainer->GetRunningState() instanceof RunningState) { - $domaincheckWasStarted = apcu_fetch($cacheKey); - // Start domaincheck again when 10 minutes are over by not returning here - if($domaincheckWasStarted !== false && is_string($domaincheckWasStarted)) { - return; - } - } - - $this->StopDomaincheckContainer(); - try { - $this->PerformRecursiveContainerStart($id); - } catch (\Exception $e) { - error_log('Could not start domaincheck container: ' . $e->getMessage()); - } - - // Cache the start for 10 minutes - apcu_add($cacheKey, '1', 600); - } - - private function StopDomaincheckContainer() : void - { - $id = 'nextcloud-aio-domaincheck'; - $this->PerformRecursiveContainerStop($id); - } +class DockerController { + private DockerActionManager $dockerActionManager; + private ContainerDefinitionFetcher $containerDefinitionFetcher; + private const string TOP_CONTAINER = 'nextcloud-aio-apache'; + private ConfigurationManager $configurationManager; + + public function __construct( + DockerActionManager $dockerActionManager, + ContainerDefinitionFetcher $containerDefinitionFetcher, + ConfigurationManager $configurationManager, + ) { + $this->dockerActionManager = $dockerActionManager; + $this->containerDefinitionFetcher = $containerDefinitionFetcher; + $this->configurationManager = $configurationManager; + } + + private function PerformRecursiveContainerStart(string $id, bool $pullImage = true) : void { + $container = $this->containerDefinitionFetcher->GetContainerById($id); + + foreach ($container->GetDependsOn() as $dependency) { + $this->PerformRecursiveContainerStart($dependency, $pullImage); + } + + // Don't start if container is already running + // This is expected to happen if a container is defined in depends_on of multiple containers + if ($container->GetRunningState() instanceof RunningState) { + error_log('Not starting ' . $id . ' because it was already started.'); + return; + } + + // Skip database image pull if the last shutdown was not clean + if ($id === 'nextcloud-aio-database') { + if ($this->dockerActionManager->GetDatabasecontainerExitCode() > 0) { + $pullImage = false; + error_log('Not pulling the latest database image because the container was not correctly shut down.'); + } + } + + // Check if docker hub is reachable in order to make sure that we do not try to pull an image if it is down + // and try to mitigate issues that are arising due to that + if ($pullImage) { + if (!$this->dockerActionManager->isDockerHubReachable($container)) { + $pullImage = false; + error_log('Not pulling the image for the ' . $container->GetContainerName() . ' container because docker hub does not seem to be reachable.'); + } + } + + $this->dockerActionManager->DeleteContainer($container); + $this->dockerActionManager->CreateVolumes($container); + if ($pullImage) { + $this->dockerActionManager->PullImage($container); + } + $this->dockerActionManager->CreateContainer($container); + $this->dockerActionManager->StartContainer($container); + $this->dockerActionManager->ConnectContainerToNetwork($container); + } + + public function GetLogs(Request $request, Response $response, array $args) : Response { + $id = $request->getQueryParams()['id']; + if (str_starts_with($id, 'nextcloud-aio-')) { + $logs = $this->dockerActionManager->GetLogs($id); + } else { + $logs = 'Container not found.'; + } + + $body = $response->getBody(); + $body->write($logs); + + return $response + ->withStatus(200) + ->withHeader('Content-Type', 'text/plain; charset=utf-8') + ->withHeader('Content-Disposition', 'inline'); + } + + public function StartBackupContainerBackup(Request $request, Response $response, array $args) : Response { + $this->startBackup(); + return $response->withStatus(201)->withHeader('Location', '/'); + } + + public function startBackup() : void { + $config = $this->configurationManager->GetConfig(); + $config['backup-mode'] = 'backup'; + $this->configurationManager->WriteConfig($config); + + $id = self::TOP_CONTAINER; + $this->PerformRecursiveContainerStop($id); + + $id = 'nextcloud-aio-borgbackup'; + $this->PerformRecursiveContainerStart($id); + } + + public function StartBackupContainerCheck(Request $request, Response $response, array $args) : Response { + $this->checkBackup(); + return $response->withStatus(201)->withHeader('Location', '/'); + } + + public function checkBackup() : void { + $config = $this->configurationManager->GetConfig(); + $config['backup-mode'] = 'check'; + $this->configurationManager->WriteConfig($config); + + $id = 'nextcloud-aio-borgbackup'; + $this->PerformRecursiveContainerStart($id); + } + + public function StartBackupContainerRestore(Request $request, Response $response, array $args) : Response { + $config = $this->configurationManager->GetConfig(); + $config['backup-mode'] = 'restore'; + $config['selected-restore-time'] = $request->getParsedBody()['selected_restore_time'] ?? ''; + $this->configurationManager->WriteConfig($config); + + $id = self::TOP_CONTAINER; + $this->PerformRecursiveContainerStop($id); + + $id = 'nextcloud-aio-borgbackup'; + $this->PerformRecursiveContainerStart($id); + + return $response->withStatus(201)->withHeader('Location', '/'); + } + + public function StartBackupContainerCheckRepair(Request $request, Response $response, array $args) : Response { + $config = $this->configurationManager->GetConfig(); + $config['backup-mode'] = 'check-repair'; + $this->configurationManager->WriteConfig($config); + + $id = 'nextcloud-aio-borgbackup'; + $this->PerformRecursiveContainerStart($id); + + // Restore to backup check which is needed to make the UI logic work correctly + $config = $this->configurationManager->GetConfig(); + $config['backup-mode'] = 'check'; + $this->configurationManager->WriteConfig($config); + + return $response->withStatus(201)->withHeader('Location', '/'); + } + + public function StartBackupContainerTest(Request $request, Response $response, array $args) : Response { + $config = $this->configurationManager->GetConfig(); + $config['backup-mode'] = 'test'; + $config['instance_restore_attempt'] = 0; + $this->configurationManager->WriteConfig($config); + + $id = self::TOP_CONTAINER; + $this->PerformRecursiveContainerStop($id); + + $id = 'nextcloud-aio-borgbackup'; + $this->PerformRecursiveContainerStart($id); + + return $response->withStatus(201)->withHeader('Location', '/'); + } + + public function StartContainer(Request $request, Response $response, array $args) : Response { + $uri = $request->getUri(); + $host = $uri->getHost(); + $port = $uri->getPort(); + if ($port === 8000) { + error_log('The AIO_URL-port was discovered to be 8000 which is not expected. It is now set to 443.'); + $port = 443; + } + + if (isset($request->getParsedBody()['install_latest_major'])) { + $installLatestMajor = 30; + } else { + $installLatestMajor = ''; + } + + $config = $this->configurationManager->GetConfig(); + // set AIO_URL + $config['AIO_URL'] = $host . ':' . $port; + // set wasStartButtonClicked + $config['wasStartButtonClicked'] = 1; + // set install_latest_major + $config['install_latest_major'] = $installLatestMajor; + $this->configurationManager->WriteConfig($config); + + // Start container + $this->startTopContainer(true); + + // Clear apcu cache in order to check if container updates are available + // Temporarily disabled as it leads much faster to docker rate limits + // apcu_clear_cache(); + + return $response->withStatus(201)->withHeader('Location', '/'); + } + + public function startTopContainer(bool $pullImage) : void { + $config = $this->configurationManager->GetConfig(); + // set AIO_TOKEN + $config['AIO_TOKEN'] = bin2hex(random_bytes(24)); + $this->configurationManager->WriteConfig($config); + + // Stop domaincheck since apache would not be able to start otherwise + $this->StopDomaincheckContainer(); + + $id = self::TOP_CONTAINER; + + $this->PerformRecursiveContainerStart($id, $pullImage); + } + + public function StartWatchtowerContainer(Request $request, Response $response, array $args) : Response { + $this->startWatchtower(); + return $response->withStatus(201)->withHeader('Location', '/'); + } + + public function startWatchtower() : void { + $id = 'nextcloud-aio-watchtower'; + + $this->PerformRecursiveContainerStart($id); + } + + private function PerformRecursiveContainerStop(string $id) : void { + $container = $this->containerDefinitionFetcher->GetContainerById($id); + foreach ($container->GetDependsOn() as $dependency) { + $this->PerformRecursiveContainerStop($dependency); + } + + // Disconnecting is not needed. This also allows to start the containers manually via docker-cli + //$this->dockerActionManager->DisconnectContainerFromNetwork($container); + $this->dockerActionManager->StopContainer($container); + } + + public function StopContainer(Request $request, Response $response, array $args) : Response { + $id = self::TOP_CONTAINER; + $this->PerformRecursiveContainerStop($id); + + return $response->withStatus(201)->withHeader('Location', '/'); + } + + public function stopTopContainer() : void { + $id = self::TOP_CONTAINER; + $this->PerformRecursiveContainerStop($id); + } + + public function StartDomaincheckContainer() : void { + # Don't start if domain is already set + if ($this->configurationManager->GetDomain() !== '' || $this->configurationManager->wasStartButtonClicked()) { + return; + } + + $id = 'nextcloud-aio-domaincheck'; + + $cacheKey = 'domaincheckWasStarted'; + + $domaincheckContainer = $this->containerDefinitionFetcher->GetContainerById($id); + $apacheContainer = $this->containerDefinitionFetcher->GetContainerById(self::TOP_CONTAINER); + // Don't start if apache is already running + if ($apacheContainer->GetRunningState() instanceof RunningState) { + return; + // Don't start if domaincheck is already running + } elseif ($domaincheckContainer->GetRunningState() instanceof RunningState) { + $domaincheckWasStarted = apcu_fetch($cacheKey); + // Start domaincheck again when 10 minutes are over by not returning here + if ($domaincheckWasStarted !== false && is_string($domaincheckWasStarted)) { + return; + } + } + + $this->StopDomaincheckContainer(); + try { + $this->PerformRecursiveContainerStart($id); + } catch (\Exception $e) { + error_log('Could not start domaincheck container: ' . $e->getMessage()); + } + + // Cache the start for 10 minutes + apcu_add($cacheKey, '1', 600); + } + + private function StopDomaincheckContainer() : void { + $id = 'nextcloud-aio-domaincheck'; + $this->PerformRecursiveContainerStop($id); + } } diff --git a/php/src/Controller/LoginController.php b/php/src/Controller/LoginController.php index 787ec6e1736..4b421434c60 100644 --- a/php/src/Controller/LoginController.php +++ b/php/src/Controller/LoginController.php @@ -3,52 +3,48 @@ namespace AIO\Controller; use AIO\Auth\AuthManager; -use AIO\Container\Container; -use AIO\ContainerDefinitionFetcher; use AIO\Docker\DockerActionManager; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; -class LoginController -{ - private AuthManager $authManager; - private DockerActionManager $dockerActionManager; - - public function __construct(AuthManager $authManager, DockerActionManager $dockerActionManager) { - $this->authManager = $authManager; - $this->dockerActionManager = $dockerActionManager; - } - - public function TryLogin(Request $request, Response $response, array $args) : Response { - if (!$this->dockerActionManager->isLoginAllowed()) { - $response->getBody()->write("The login is blocked since Nextcloud is running."); - return $response->withHeader('Location', '/')->withStatus(422); - } - $password = $request->getParsedBody()['password'] ?? ''; - if($this->authManager->CheckCredentials($password)) { - $this->authManager->SetAuthState(true); - return $response->withHeader('Location', '/')->withStatus(201); - } - - $response->getBody()->write("The password is incorrect."); - return $response->withHeader('Location', '/')->withStatus(422); - } - - public function GetTryLogin(Request $request, Response $response, array $args) : Response { - $token = $request->getQueryParams()['token'] ?? ''; - if($this->authManager->CheckToken($token)) { - $this->authManager->SetAuthState(true); - return $response->withHeader('Location', '/')->withStatus(302); - } - - return $response->withHeader('Location', '/')->withStatus(302); - } - - public function Logout(Request $request, Response $response, array $args) : Response - { - $this->authManager->SetAuthState(false); - return $response - ->withHeader('Location', '/') - ->withStatus(302); - } +class LoginController { + private AuthManager $authManager; + private DockerActionManager $dockerActionManager; + + public function __construct(AuthManager $authManager, DockerActionManager $dockerActionManager) { + $this->authManager = $authManager; + $this->dockerActionManager = $dockerActionManager; + } + + public function TryLogin(Request $request, Response $response, array $args) : Response { + if (!$this->dockerActionManager->isLoginAllowed()) { + $response->getBody()->write('The login is blocked since Nextcloud is running.'); + return $response->withHeader('Location', '/')->withStatus(422); + } + $password = $request->getParsedBody()['password'] ?? ''; + if ($this->authManager->CheckCredentials($password)) { + $this->authManager->SetAuthState(true); + return $response->withHeader('Location', '/')->withStatus(201); + } + + $response->getBody()->write('The password is incorrect.'); + return $response->withHeader('Location', '/')->withStatus(422); + } + + public function GetTryLogin(Request $request, Response $response, array $args) : Response { + $token = $request->getQueryParams()['token'] ?? ''; + if ($this->authManager->CheckToken($token)) { + $this->authManager->SetAuthState(true); + return $response->withHeader('Location', '/')->withStatus(302); + } + + return $response->withHeader('Location', '/')->withStatus(302); + } + + public function Logout(Request $request, Response $response, array $args) : Response { + $this->authManager->SetAuthState(false); + return $response + ->withHeader('Location', '/') + ->withStatus(302); + } } diff --git a/php/src/Cron/BackupNotification.php b/php/src/Cron/BackupNotification.php index 17da93b2ab8..2f493ea90c2 100644 --- a/php/src/Cron/BackupNotification.php +++ b/php/src/Cron/BackupNotification.php @@ -1,33 +1,33 @@ -get(\AIO\Docker\DockerActionManager::class); -/** @var \AIO\ContainerDefinitionFetcher $containerDefinitionFetcher */ -$containerDefinitionFetcher = $container->get(\AIO\ContainerDefinitionFetcher::class); - -$id = 'nextcloud-aio-nextcloud'; -$nextcloudContainer = $containerDefinitionFetcher->GetContainerById($id); - -$backupExitCode = $dockerActionManger->GetBackupcontainerExitCode(); - -if ($backupExitCode === 0) { - if (getenv('SEND_SUCCESS_NOTIFICATIONS') === "0") { - error_log("Daily backup successful! Only logging successful backup and not sending backup notification since that has been disabled! You can get further info by looking at the backup logs in the AIO interface."); - } else { - $dockerActionManger->sendNotification($nextcloudContainer, 'Daily backup successful!', 'You can get further info by looking at the backup logs in the AIO interface.'); - } -} - -if ($backupExitCode > 0) { - $dockerActionManger->sendNotification($nextcloudContainer, 'Daily backup failed!', 'You can get further info by looking at the backup logs in the AIO interface.'); -} +get(\AIO\Docker\DockerActionManager::class); +/** @var \AIO\ContainerDefinitionFetcher $containerDefinitionFetcher */ +$containerDefinitionFetcher = $container->get(\AIO\ContainerDefinitionFetcher::class); + +$id = 'nextcloud-aio-nextcloud'; +$nextcloudContainer = $containerDefinitionFetcher->GetContainerById($id); + +$backupExitCode = $dockerActionManger->GetBackupcontainerExitCode(); + +if ($backupExitCode === 0) { + if (getenv('SEND_SUCCESS_NOTIFICATIONS') === '0') { + error_log('Daily backup successful! Only logging successful backup and not sending backup notification since that has been disabled! You can get further info by looking at the backup logs in the AIO interface.'); + } else { + $dockerActionManger->sendNotification($nextcloudContainer, 'Daily backup successful!', 'You can get further info by looking at the backup logs in the AIO interface.'); + } +} + +if ($backupExitCode > 0) { + $dockerActionManger->sendNotification($nextcloudContainer, 'Daily backup failed!', 'You can get further info by looking at the backup logs in the AIO interface.'); +} diff --git a/php/src/Cron/CheckBackup.php b/php/src/Cron/CheckBackup.php index 6d9b027caf0..955a1d16b2c 100644 --- a/php/src/Cron/CheckBackup.php +++ b/php/src/Cron/CheckBackup.php @@ -1,17 +1,18 @@ -get(\AIO\Controller\DockerController::class); - -// Stop container and start backup check -$dockerController->checkBackup(); +get(\AIO\Controller\DockerController::class); + +// Stop container and start backup check +$dockerController->checkBackup(); diff --git a/php/src/Cron/CheckFreeDiskSpace.php b/php/src/Cron/CheckFreeDiskSpace.php index b462195ec78..ec56159d427 100644 --- a/php/src/Cron/CheckFreeDiskSpace.php +++ b/php/src/Cron/CheckFreeDiskSpace.php @@ -1,10 +1,10 @@ sendNotification($nextcloudContainer, 'Low on space!', 'The drive that hosts the mastercontainer volume has less than 5 GB free space. Container updates and backups might not succeed due to that!'); + error_log('The drive that hosts the mastercontainer volume has less than 5 GB free space. Container updates and backups might not succeed due to that!'); + $dockerActionManger->sendNotification($nextcloudContainer, 'Low on space!', 'The drive that hosts the mastercontainer volume has less than 5 GB free space. Container updates and backups might not succeed due to that!'); } diff --git a/php/src/Cron/CreateBackup.php b/php/src/Cron/CreateBackup.php index 60366973cb1..3f23e7d2e4c 100644 --- a/php/src/Cron/CreateBackup.php +++ b/php/src/Cron/CreateBackup.php @@ -1,17 +1,18 @@ -get(\AIO\Controller\DockerController::class); - -// Stop container and start backup -$dockerController->startBackup(); +get(\AIO\Controller\DockerController::class); + +// Stop container and start backup +$dockerController->startBackup(); diff --git a/php/src/Cron/OutdatedNotification.php b/php/src/Cron/OutdatedNotification.php index e652ba3a12c..ec2165361c0 100644 --- a/php/src/Cron/OutdatedNotification.php +++ b/php/src/Cron/OutdatedNotification.php @@ -1,10 +1,10 @@ isNextcloudImageOutdated(); if ($isNextcloudImageOutdated === true) { - $dockerActionManger->sendNotification($nextcloudContainer, 'AIO is outdated!', 'Please open the AIO interface or ask an administrator to update it. If you do not want to do it manually each time, you can enable the daily backup feature from the AIO interface which automatically updates all containers.', '/notify-all.sh'); + $dockerActionManger->sendNotification($nextcloudContainer, 'AIO is outdated!', 'Please open the AIO interface or ask an administrator to update it. If you do not want to do it manually each time, you can enable the daily backup feature from the AIO interface which automatically updates all containers.', '/notify-all.sh'); } - diff --git a/php/src/Cron/StartAndUpdateContainers.php b/php/src/Cron/StartAndUpdateContainers.php index 049245afebc..0c0a4ad4a6b 100644 --- a/php/src/Cron/StartAndUpdateContainers.php +++ b/php/src/Cron/StartAndUpdateContainers.php @@ -1,20 +1,20 @@ -get(\AIO\Controller\DockerController::class); - -// Start apache -$dockerController->startTopContainer(true); +get(\AIO\Controller\DockerController::class); + +// Start apache +$dockerController->startTopContainer(true); diff --git a/php/src/Cron/StartContainers.php b/php/src/Cron/StartContainers.php index 366866ad934..56403ae181d 100644 --- a/php/src/Cron/StartContainers.php +++ b/php/src/Cron/StartContainers.php @@ -1,20 +1,20 @@ -get(\AIO\Controller\DockerController::class); - -// Start apache -$dockerController->startTopContainer(false); +get(\AIO\Controller\DockerController::class); + +// Start apache +$dockerController->startTopContainer(false); diff --git a/php/src/Cron/StopContainers.php b/php/src/Cron/StopContainers.php index 89cf9798a37..ea8443d729c 100644 --- a/php/src/Cron/StopContainers.php +++ b/php/src/Cron/StopContainers.php @@ -1,17 +1,17 @@ -get(\AIO\Controller\DockerController::class); - -// Start apache -$dockerController->stopTopContainer(); +get(\AIO\Controller\DockerController::class); + +// Start apache +$dockerController->stopTopContainer(); diff --git a/php/src/Cron/UpdateMastercontainer.php b/php/src/Cron/UpdateMastercontainer.php index 01c28e5988e..49045968244 100644 --- a/php/src/Cron/UpdateMastercontainer.php +++ b/php/src/Cron/UpdateMastercontainer.php @@ -1,17 +1,17 @@ -get(\AIO\Controller\DockerController::class); - -# Update the mastercontainer -$dockerController->startWatchtower(); +get(\AIO\Controller\DockerController::class); + +# Update the mastercontainer +$dockerController->startWatchtower(); diff --git a/php/src/Cron/UpdateNotification.php b/php/src/Cron/UpdateNotification.php index e1d57f6a734..43ba10f501c 100644 --- a/php/src/Cron/UpdateNotification.php +++ b/php/src/Cron/UpdateNotification.php @@ -1,10 +1,10 @@ isAnyUpdateAvailable(); if ($isMastercontainerUpdateAvailable === true) { - $dockerActionManger->sendNotification($nextcloudContainer, 'Mastercontainer update available!', 'Please open your AIO interface to update it. If you do not want to do it manually each time, you can enable the daily backup feature from the AIO interface which also automatically updates the mastercontainer.'); + $dockerActionManger->sendNotification($nextcloudContainer, 'Mastercontainer update available!', 'Please open your AIO interface to update it. If you do not want to do it manually each time, you can enable the daily backup feature from the AIO interface which also automatically updates the mastercontainer.'); } if ($isAnyUpdateAvailable === true) { - $dockerActionManger->sendNotification($nextcloudContainer, 'Container updates available!', 'Please open your AIO interface to update them. If you do not want to do it manually each time, you can enable the daily backup feature from the AIO interface which also automatically updates your containers and your Nextcloud apps.'); + $dockerActionManger->sendNotification($nextcloudContainer, 'Container updates available!', 'Please open your AIO interface to update them. If you do not want to do it manually each time, you can enable the daily backup feature from the AIO interface which also automatically updates your containers and your Nextcloud apps.'); } diff --git a/php/src/Data/ConfigurationManager.php b/php/src/Data/ConfigurationManager.php index 527904fbfed..f9f6f642614 100644 --- a/php/src/Data/ConfigurationManager.php +++ b/php/src/Data/ConfigurationManager.php @@ -2,952 +2,946 @@ namespace AIO\Data; -use AIO\Auth\PasswordGenerator; -use AIO\Controller\DockerController; - -class ConfigurationManager -{ - public function GetConfig() : array - { - if(file_exists(DataConst::GetConfigFile())) - { - $configContent = file_get_contents(DataConst::GetConfigFile()); - return json_decode($configContent, true, 512, JSON_THROW_ON_ERROR); - } - - return []; - } - - public function GetPassword() : string { - return $this->GetConfig()['password']; - } - - public function GetToken() : string { - return $this->GetConfig()['AIO_TOKEN']; - } - - public function SetPassword(string $password) : void { - $config = $this->GetConfig(); - $config['password'] = $password; - $this->WriteConfig($config); - } - - public function GetAndGenerateSecret(string $secretId) : string { - $config = $this->GetConfig(); - if(!isset($config['secrets'][$secretId])) { - $config['secrets'][$secretId] = bin2hex(random_bytes(24)); - $this->WriteConfig($config); - } - - if ($secretId === 'BORGBACKUP_PASSWORD' && !file_exists(DataConst::GetBackupSecretFile())) { - $this->DoubleSafeBackupSecret($config['secrets'][$secretId]); - } - - return $config['secrets'][$secretId]; - } - - public function GetSecret(string $secretId) : string { - $config = $this->GetConfig(); - if(!isset($config['secrets'][$secretId])) { - $config['secrets'][$secretId] = ""; - } - - return $config['secrets'][$secretId]; - } - - private function DoubleSafeBackupSecret(string $borgBackupPassword) : void { - file_put_contents(DataConst::GetBackupSecretFile(), $borgBackupPassword); - } - - public function hasBackupRunOnce() : bool { - if (!file_exists(DataConst::GetBackupKeyFile())) { - return false; - } else { - return true; - } - } - - public function GetLastBackupTime() : string { - if (!file_exists(DataConst::GetBackupArchivesList())) { - return ''; - } - - $content = file_get_contents(DataConst::GetBackupArchivesList()); - if ($content === '') { - return ''; - } - - $lastBackupLines = explode("\n", $content); - $lastBackupLine = ""; - if (count($lastBackupLines) >= 2) { - $lastBackupLine = $lastBackupLines[sizeof($lastBackupLines) - 2]; - } - if ($lastBackupLine === "") { - return ''; - } - - $lastBackupTimes = explode(",", $lastBackupLine); - $lastBackupTime = $lastBackupTimes[1]; - if ($lastBackupTime === "") { - return ''; - } - - return $lastBackupTime; - } - - public function GetBackupTimes() : array { - if (!file_exists(DataConst::GetBackupArchivesList())) { - return []; - } - - $content = file_get_contents(DataConst::GetBackupArchivesList()); - if ($content === '') { - return []; - } - - $backupLines = explode("\n", $content); - $backupTimes = []; - foreach($backupLines as $lines) { - if ($lines !== "") { - $backupTimesTemp = explode(',', $lines); - $backupTimes[] = $backupTimesTemp[1]; - } - } - - // Reverse the array to list newest backup first - $backupTimes = array_reverse($backupTimes); - - return $backupTimes; - } - - public function wasStartButtonClicked() : bool { - if (isset($this->GetConfig()['wasStartButtonClicked'])) { - return true; - } else { - return false; - } - } - - public function isx64Platform() : bool { - if (php_uname('m') === 'x86_64') { - return true; - } else { - return false; - } - } - - public function isClamavEnabled() : bool { - if (!$this->isx64Platform()) { - return false; - } - - $config = $this->GetConfig(); - if (isset($config['isClamavEnabled']) && $config['isClamavEnabled'] === 1) { - return true; - } else { - return false; - } - } - - public function isDockerSocketProxyEnabled() : bool { - $config = $this->GetConfig(); - if (isset($config['isDockerSocketProxyEnabled']) && $config['isDockerSocketProxyEnabled'] === 1) { - return true; - } else { - return false; - } - } - - public function SetDockerSocketProxyEnabledState(int $value) : void { - $config = $this->GetConfig(); - $config['isDockerSocketProxyEnabled'] = $value; - $this->WriteConfig($config); - } - - public function isWhiteboardEnabled() : bool { - $config = $this->GetConfig(); - if (isset($config['isWhiteboardEnabled']) && $config['isWhiteboardEnabled'] === 1) { - return true; - } else { - return false; - } - } - - public function SetWhiteboardEnabledState(int $value) : void { - $config = $this->GetConfig(); - $config['isWhiteboardEnabled'] = $value; - $this->WriteConfig($config); - } - - public function SetClamavEnabledState(int $value) : void { - $config = $this->GetConfig(); - $config['isClamavEnabled'] = $value; - $this->WriteConfig($config); - } - - public function isImaginaryEnabled() : bool { - $config = $this->GetConfig(); - if (isset($config['isImaginaryEnabled']) && $config['isImaginaryEnabled'] === 0) { - return false; - } else { - return true; - } - } - - public function SetImaginaryEnabledState(int $value) : void { - $config = $this->GetConfig(); - $config['isImaginaryEnabled'] = $value; - $this->WriteConfig($config); - } - - public function isFulltextsearchEnabled() : bool { - $config = $this->GetConfig(); - if (isset($config['isFulltextsearchEnabled']) && $config['isFulltextsearchEnabled'] === 1) { - return true; - } else { - return false; - } - } - - public function SetFulltextsearchEnabledState(int $value) : void { - $config = $this->GetConfig(); - $config['isFulltextsearchEnabled'] = $value; - $this->WriteConfig($config); - } - - public function isOnlyofficeEnabled() : bool { - $config = $this->GetConfig(); - if (isset($config['isOnlyofficeEnabled']) && $config['isOnlyofficeEnabled'] === 1) { - return true; - } else { - return false; - } - } - - public function SetOnlyofficeEnabledState(int $value) : void { - $config = $this->GetConfig(); - $config['isOnlyofficeEnabled'] = $value; - $this->WriteConfig($config); - } - - public function isCollaboraEnabled() : bool { - $config = $this->GetConfig(); - if (isset($config['isCollaboraEnabled']) && $config['isCollaboraEnabled'] === 0) { - return false; - } else { - return true; - } - } - - public function SetCollaboraEnabledState(int $value) : void { - $config = $this->GetConfig(); - $config['isCollaboraEnabled'] = $value; - $this->WriteConfig($config); - } - - public function isTalkEnabled() : bool { - $config = $this->GetConfig(); - if (isset($config['isTalkEnabled']) && $config['isTalkEnabled'] === 0) { - return false; - } else { - return true; - } - } - - public function SetTalkEnabledState(int $value) : void { - $config = $this->GetConfig(); - $config['isTalkEnabled'] = $value; - $this->WriteConfig($config); - } - - public function isTalkRecordingEnabled() : bool { - if (!$this->isTalkEnabled()) { - return false; - } - $config = $this->GetConfig(); - if (isset($config['isTalkRecordingEnabled']) && $config['isTalkRecordingEnabled'] === 1) { - return true; - } else { - return false; - } - } - - public function SetTalkRecordingEnabledState(int $value) : void { - if (!$this->isTalkEnabled()) { - $value = 0; - } - $config = $this->GetConfig(); - $config['isTalkRecordingEnabled'] = $value; - $this->WriteConfig($config); - } - - /** - * @throws InvalidSettingConfigurationException - */ - public function SetDomain(string $domain) : void { - // Validate that at least one dot is contained - if (!str_contains($domain, '.')) { - throw new InvalidSettingConfigurationException("Domain must contain at least one dot!"); - } - - // Validate that no slashes are contained - if (str_contains($domain, '/')) { - throw new InvalidSettingConfigurationException("Domain must not contain slashes!"); - } - - // Validate that no colons are contained - if (str_contains($domain, ':')) { - throw new InvalidSettingConfigurationException("Domain must not contain colons!"); - } - - // Validate domain - if (filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) === false) { - throw new InvalidSettingConfigurationException("Domain is not a valid domain!"); - } - - // Validate that it is not an IP-address - if(filter_var($domain, FILTER_VALIDATE_IP)) { - throw new InvalidSettingConfigurationException("Please enter a domain and not an IP-address!"); - } - - // Skip domain validation if opted in to do so - if (!$this->shouldDomainValidationBeSkipped()) { - - $dnsRecordIP = gethostbyname($domain); - if ($dnsRecordIP === $domain) { - $dnsRecordIP = ''; - } - - if (empty($dnsRecordIP)) { - $record = dns_get_record($domain, DNS_AAAA); - if (isset($record[0]['ipv6']) && !empty($record[0]['ipv6'])) { - $dnsRecordIP = $record[0]['ipv6']; - } - } - - // Validate IP - if (!filter_var($dnsRecordIP, FILTER_VALIDATE_IP)) { - throw new InvalidSettingConfigurationException("DNS config is not set for this domain or the domain is not a valid domain! (It was found to be set to '" . $dnsRecordIP . "')"); - } - - // Get the apache port - $port = $this->GetApachePort(); - - if (!filter_var($dnsRecordIP, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { - if ($port === '443') { - throw new InvalidSettingConfigurationException("It seems like the ip-address of the domain is set to an internal or reserved ip-address. This is not supported. (It was found to be set to '" . $dnsRecordIP . "'). Please set it to a public ip-address so that the domain validation can work!"); - } else { - error_log("It seems like the ip-address of " . $domain . " is set to an internal or reserved ip-address. (It was found to be set to '" . $dnsRecordIP . "')"); - } - } - - // Check if port 443 is open - $connection = @fsockopen($domain, 443, $errno, $errstr, 10); - if ($connection) { - fclose($connection); - } else { - throw new InvalidSettingConfigurationException("The domain is not reachable on Port 443 from within this container. Have you opened port 443/tcp in your router/firewall? If yes is the problem most likely that the router or firewall forbids local access to your domain. You can work around that by setting up a local DNS-server."); - } - - // Get Instance ID - $instanceID = $this->GetAndGenerateSecret('INSTANCE_ID'); - - // set protocol - if ($port !== '443') { - $protocol = 'https://'; - } else { - $protocol = 'http://'; - } - - // Check if response is correct - $ch = curl_init(); - $testUrl = $protocol . $domain . ':443'; - curl_setopt($ch, CURLOPT_URL, $testUrl); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); - curl_setopt($ch, CURLOPT_TIMEOUT, 10); - $response = (string)curl_exec($ch); - # Get rid of trailing \n - $response = str_replace("\n", "", $response); - - if ($response !== $instanceID) { - error_log('The response of the connection attempt to "' . $testUrl . '" was: ' . $response); - error_log('Expected was: ' . $instanceID); - error_log('The error message was: ' . curl_error($ch)); - $notice = "Domain does not point to this server or the reverse proxy is not configured correctly. See the mastercontainer logs for more details. ('sudo docker logs -f nextcloud-aio-mastercontainer')"; - if ($port === '443') { - $notice .= " If you should be using Cloudflare, make sure to disable the Cloudflare Proxy feature as it might block the domain validation. Same for any other firewall or service that blocks unencrypted access on port 443."; - } else { - error_log('Please follow https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md#6-how-to-debug-things in order to debug things!'); - } - throw new InvalidSettingConfigurationException($notice); - } - } - - // Write domain - $config = $this->GetConfig(); - $config['domain'] = $domain; - // Reset the borg restore password when setting the domain - $config['borg_restore_password'] = ''; - $this->WriteConfig($config); - } - - public function GetDomain() : string { - $config = $this->GetConfig(); - if(!isset($config['domain'])) { - $config['domain'] = ''; - } - - return $config['domain']; - } - - public function GetBaseDN() : string { - $domain = $this->GetDomain(); - if ($domain === "") { - return ""; - } - return 'dc=' . implode(',dc=', explode('.', $domain)); - } - - public function GetBackupMode() : string { - $config = $this->GetConfig(); - if(!isset($config['backup-mode'])) { - $config['backup-mode'] = ''; - } - - return $config['backup-mode']; - } - - public function GetSelectedRestoreTime() : string { - $config = $this->GetConfig(); - if(!isset($config['selected-restore-time'])) { - $config['selected-restore-time'] = ''; - } - - return $config['selected-restore-time']; - } - - public function GetAIOURL() : string { - $config = $this->GetConfig(); - if(!isset($config['AIO_URL'])) { - $config['AIO_URL'] = ''; - } - - return $config['AIO_URL']; - } - - /** - * @throws InvalidSettingConfigurationException - */ - public function SetBorgBackupHostLocation(string $location) : void { - $isValidPath = false; - if (str_starts_with($location, '/') && !str_ends_with($location, '/')) { - $isValidPath = true; - } elseif ($location === 'nextcloud_aio_backupdir') { - $isValidPath = true; - } - - if (!$isValidPath) { - throw new InvalidSettingConfigurationException("The path must start with '/', and must not end with '/'!"); - } - - - $config = $this->GetConfig(); - $config['borg_backup_host_location'] = $location; - $this->WriteConfig($config); - } - - public function DeleteBorgBackupHostLocation() : void { - $config = $this->GetConfig(); - $config['borg_backup_host_location'] = ''; - $this->WriteConfig($config); - } - - /** - * @throws InvalidSettingConfigurationException - */ - public function SetBorgRestoreHostLocationAndPassword(string $location, string $password) : void { - if ($location === '') { - throw new InvalidSettingConfigurationException("Please enter a path!"); - } - - $isValidPath = false; - if (str_starts_with($location, '/') && !str_ends_with($location, '/')) { - $isValidPath = true; - } elseif ($location === 'nextcloud_aio_backupdir') { - $isValidPath = true; - } - - if (!$isValidPath) { - throw new InvalidSettingConfigurationException("The path must start with '/', and must not end with '/'!"); - } - - if ($password === '') { - throw new InvalidSettingConfigurationException("Please enter the password!"); - } - - $config = $this->GetConfig(); - $config['borg_backup_host_location'] = $location; - $config['borg_restore_password'] = $password; - $config['instance_restore_attempt'] = 1; - $this->WriteConfig($config); - } - - /** - * @throws InvalidSettingConfigurationException - */ - public function ChangeMasterPassword(string $currentPassword, string $newPassword) : void { - if ($currentPassword === '') { - throw new InvalidSettingConfigurationException("Please enter your current password."); - } - - if ($currentPassword !== $this->GetPassword()) { - throw new InvalidSettingConfigurationException("The entered current password is not correct."); - } - - if ($newPassword === '') { - throw new InvalidSettingConfigurationException("Please enter a new password."); - } - - if (strlen($newPassword) < 24) { - throw new InvalidSettingConfigurationException("New passwords must be >= 24 digits."); - } - - if (!preg_match("#^[a-zA-Z0-9 ]+$#", $newPassword)) { - throw new InvalidSettingConfigurationException('Not allowed characters in the new password.'); - } - - // All checks pass so set the password - $this->SetPassword($newPassword); - } - - public function GetApachePort() : string { - $envVariableName = 'APACHE_PORT'; - $configName = 'apache_port'; - $defaultValue = '443'; - return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); - } - - public function GetTalkPort() : string { - $envVariableName = 'TALK_PORT'; - $configName = 'talk_port'; - $defaultValue = '3478'; - return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); - } - - /** - * @throws InvalidSettingConfigurationException - */ - public function WriteConfig(array $config) : void { - if(!is_dir(DataConst::GetDataDirectory())) { - throw new InvalidSettingConfigurationException(DataConst::GetDataDirectory() . " does not exist! Something was set up falsely!"); - } - $df = disk_free_space(DataConst::GetDataDirectory()); - $content = json_encode($config, JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR); - $size = strlen($content) + 10240; - if ($df !== false && (int)$df < $size) { - throw new InvalidSettingConfigurationException(DataConst::GetDataDirectory() . " does not have enough space for writing the config file! Not writing it back!"); - } - file_put_contents(DataConst::GetConfigFile(), $content); - } - - private function GetEnvironmentalVariableOrConfig(string $envVariableName, string $configName, string $defaultValue) : string { - $envVariableOutput = getenv($envVariableName); - if ($envVariableOutput === false) { - $config = $this->GetConfig(); - if (!isset($config[$configName]) || $config[$configName] === '') { - $config[$configName] = $defaultValue; - } - return $config[$configName]; - } - if(file_exists(DataConst::GetConfigFile())) { - $config = $this->GetConfig(); - if (!isset($config[$configName])) { - $config[$configName] = ''; - } - if ($envVariableOutput !== $config[$configName]) { - $config[$configName] = $envVariableOutput; - $this->WriteConfig($config); - } - } - return $envVariableOutput; - } - - public function GetBorgBackupHostLocation() : string { - $config = $this->GetConfig(); - if(!isset($config['borg_backup_host_location'])) { - $config['borg_backup_host_location'] = ''; - } - - return $config['borg_backup_host_location']; - } - - public function GetBorgRestorePassword() : string { - $config = $this->GetConfig(); - if(!isset($config['borg_restore_password'])) { - $config['borg_restore_password'] = ''; - } - - return $config['borg_restore_password']; - } - - public function isInstanceRestoreAttempt() : bool { - $config = $this->GetConfig(); - if(!isset($config['instance_restore_attempt'])) { - $config['instance_restore_attempt'] = ''; - } - - if ($config['instance_restore_attempt'] === 1) { - return true; - } - return false; - } - - public function GetBorgBackupMode() : string { - $config = $this->GetConfig(); - if(!isset($config['backup-mode'])) { - $config['backup-mode'] = ''; - } - - return $config['backup-mode']; - } - - public function GetNextcloudMount() : string { - $envVariableName = 'NEXTCLOUD_MOUNT'; - $configName = 'nextcloud_mount'; - $defaultValue = ''; - return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); - } - - public function GetNextcloudDatadirMount() : string { - $envVariableName = 'NEXTCLOUD_DATADIR'; - $configName = 'nextcloud_datadir'; - $defaultValue = 'nextcloud_aio_nextcloud_data'; - return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); - } - - public function GetNextcloudUploadLimit() : string { - $envVariableName = 'NEXTCLOUD_UPLOAD_LIMIT'; - $configName = 'nextcloud_upload_limit'; - $defaultValue = '10G'; - return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); - } - - public function GetNextcloudMemoryLimit() : string { - $envVariableName = 'NEXTCLOUD_MEMORY_LIMIT'; - $configName = 'nextcloud_memory_limit'; - $defaultValue = '512M'; - return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); - } - - public function GetApacheMaxSize() : int { - $uploadLimit = (int)rtrim($this->GetNextcloudUploadLimit(), 'G'); - return $uploadLimit * 1024 * 1024 * 1024; - } - - public function GetNextcloudMaxTime() : string { - $envVariableName = 'NEXTCLOUD_MAX_TIME'; - $configName = 'nextcloud_max_time'; - $defaultValue = '3600'; - return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); - } - - public function GetBorgRetentionPolicy() : string { - $envVariableName = 'BORG_RETENTION_POLICY'; - $configName = 'borg_retention_policy'; - $defaultValue = '--keep-within=7d --keep-weekly=4 --keep-monthly=6'; - return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); - } - - public function GetDockerSocketPath() : string { - $envVariableName = 'WATCHTOWER_DOCKER_SOCKET_PATH'; - $configName = 'docker_socket_path'; - $defaultValue = '/var/run/docker.sock'; - return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); - } - - public function GetTrustedCacertsDir() : string { - $envVariableName = 'NEXTCLOUD_TRUSTED_CACERTS_DIR'; - $configName = 'trusted_cacerts_dir'; - $defaultValue = ''; - return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); - } - - public function GetNextcloudAdditionalApks() : string { - $envVariableName = 'NEXTCLOUD_ADDITIONAL_APKS'; - $configName = 'nextcloud_additional_apks'; - $defaultValue = 'imagemagick'; - return trim($this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue)); - } - - public function GetNextcloudAdditionalPhpExtensions() : string { - $envVariableName = 'NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS'; - $configName = 'nextcloud_additional_php_extensions'; - $defaultValue = 'imagick'; - return trim($this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue)); - } - - public function GetCollaboraSeccompPolicy() : string { - $defaultString = '--o:security.seccomp='; - if ($this->GetCollaboraSeccompDisabledState() !== 'true') { - return $defaultString . 'true'; - } - return $defaultString . 'false'; - } - - private function GetCollaboraSeccompDisabledState() : string { - $envVariableName = 'COLLABORA_SECCOMP_DISABLED'; - $configName = 'collabora_seccomp_disabled'; - $defaultValue = 'false'; - return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); - } - - /** - * @throws InvalidSettingConfigurationException - */ - public function SetDailyBackupTime(string $time, bool $enableAutomaticUpdates, bool $successNotification) : void { - if ($time === "") { - throw new InvalidSettingConfigurationException("The daily backup time must not be empty!"); - } - - if (!preg_match("#^[0-1][0-9]:[0-5][0-9]$#", $time) && !preg_match("#^2[0-3]:[0-5][0-9]$#", $time)) { - throw new InvalidSettingConfigurationException("You did not enter a correct time! One correct example is '04:00'!"); - } - - if ($enableAutomaticUpdates === false) { - $time .= PHP_EOL . 'automaticUpdatesAreNotEnabled'; - } else { - $time .= PHP_EOL; - } - if ($successNotification === false) { - $time .= PHP_EOL . 'successNotificationsAreNotEnabled'; - } else { - $time .= PHP_EOL; - } - file_put_contents(DataConst::GetDailyBackupTimeFile(), $time); - } - - public function GetDailyBackupTime() : string { - if (!file_exists(DataConst::GetDailyBackupTimeFile())) { - return ''; - } - $dailyBackupFile = file_get_contents(DataConst::GetDailyBackupTimeFile()); - $dailyBackupFileArray = explode("\n", $dailyBackupFile); - return $dailyBackupFileArray[0]; - } - - public function areAutomaticUpdatesEnabled() : bool { - if (!file_exists(DataConst::GetDailyBackupTimeFile())) { - return false; - } - $dailyBackupFile = file_get_contents(DataConst::GetDailyBackupTimeFile()); - $dailyBackupFileArray = explode("\n", $dailyBackupFile); - if (isset($dailyBackupFileArray[1]) && $dailyBackupFileArray[1] === 'automaticUpdatesAreNotEnabled') { - return false; - } else { - return true; - } - } - - public function DeleteDailyBackupTime() : void { - if (file_exists(DataConst::GetDailyBackupTimeFile())) { - unlink(DataConst::GetDailyBackupTimeFile()); - } - } - - /** - * @throws InvalidSettingConfigurationException - */ - public function SetAdditionalBackupDirectories(string $additionalBackupDirectories) : void { - $additionalBackupDirectoriesArray = explode("\n", $additionalBackupDirectories); - $validDirectories = ''; - foreach($additionalBackupDirectoriesArray as $entry) { - // Trim all unwanted chars on both sites - $entry = trim($entry); - if ($entry !== "") { - if (!preg_match("#^/[.0-9a-zA-Z/_-]+$#", $entry) && !preg_match("#^[.0-9a-zA-Z_-]+$#", $entry)) { - throw new InvalidSettingConfigurationException("You entered unallowed characters! Problematic is " . $entry); - } - $validDirectories .= rtrim($entry, '/') . PHP_EOL; - } - } - - if ($validDirectories === '') { - unlink(DataConst::GetAdditionalBackupDirectoriesFile()); - } else { - file_put_contents(DataConst::GetAdditionalBackupDirectoriesFile(), $validDirectories); - } - } - - public function shouldLatestMajorGetInstalled() : bool { - $config = $this->GetConfig(); - if(!isset($config['install_latest_major'])) { - $config['install_latest_major'] = ''; - } - return $config['install_latest_major'] !== ''; - } - - public function GetAdditionalBackupDirectoriesString() : string { - if (!file_exists(DataConst::GetAdditionalBackupDirectoriesFile())) { - return ''; - } - $additionalBackupDirectories = file_get_contents(DataConst::GetAdditionalBackupDirectoriesFile()); - return $additionalBackupDirectories; - } - - public function GetAdditionalBackupDirectoriesArray() : array { - $additionalBackupDirectories = $this->GetAdditionalBackupDirectoriesString(); - $additionalBackupDirectoriesArray = explode("\n", $additionalBackupDirectories); - $additionalBackupDirectoriesArray = array_unique($additionalBackupDirectoriesArray, SORT_REGULAR); - return $additionalBackupDirectoriesArray; - } - - public function isDailyBackupRunning() : bool { - if (file_exists(DataConst::GetDailyBackupBlockFile())) { - return true; - } - return false; - } - - public function GetTimezone() : string { - $config = $this->GetConfig(); - if(!isset($config['timezone'])) { - $config['timezone'] = ''; - } - - return $config['timezone']; - } - - /** - * @throws InvalidSettingConfigurationException - */ - public function SetTimezone(string $timezone) : void { - if ($timezone === "") { - throw new InvalidSettingConfigurationException("The timezone must not be empty!"); - } - - if (!preg_match("#^[a-zA-Z0-9_\-\/\+]+$#", $timezone)) { - throw new InvalidSettingConfigurationException("The entered timezone does not seem to be a valid timezone!"); - } - - $config = $this->GetConfig(); - $config['timezone'] = $timezone; - $this->WriteConfig($config); - } - - public function DeleteTimezone() : void { - $config = $this->GetConfig(); - $config['timezone'] = ''; - $this->WriteConfig($config); - } - - public function shouldDomainValidationBeSkipped() : bool { - if (getenv('SKIP_DOMAIN_VALIDATION') !== false) { - return true; - } - return false; - } - - public function GetNextcloudStartupApps() : string { - $apps = getenv('NEXTCLOUD_STARTUP_APPS'); - if (is_string($apps)) { - return trim($apps); - } - return 'deck twofactor_totp tasks calendar contacts notes'; - } - - public function GetCollaboraDictionaries() : string { - $config = $this->GetConfig(); - if(!isset($config['collabora_dictionaries'])) { - $config['collabora_dictionaries'] = ''; - } - - return $config['collabora_dictionaries']; - } - - /** - * @throws InvalidSettingConfigurationException - */ - public function SetCollaboraDictionaries(string $CollaboraDictionaries) : void { - if ($CollaboraDictionaries === "") { - throw new InvalidSettingConfigurationException("The dictionaries must not be empty!"); - } - - if (!preg_match("#^[a-zA-Z_ ]+$#", $CollaboraDictionaries)) { - throw new InvalidSettingConfigurationException("The entered dictionaries do not seem to be a valid!"); - } - - $config = $this->GetConfig(); - $config['collabora_dictionaries'] = $CollaboraDictionaries; - $this->WriteConfig($config); - } - - public function DeleteCollaboraDictionaries() : void { - $config = $this->GetConfig(); - $config['collabora_dictionaries'] = ''; - $this->WriteConfig($config); - } - - public function GetApacheIPBinding() : string { - $envVariableName = 'APACHE_IP_BINDING'; - $configName = 'apache_ip_binding'; - $defaultValue = ''; - return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); - } - - private function GetDisableBackupSection() : string { - $envVariableName = 'AIO_DISABLE_BACKUP_SECTION'; - $configName = 'disable_backup_section'; - $defaultValue = ''; - return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); - } - - public function isBackupSectionEnabled() : bool { - if ($this->GetDisableBackupSection() === 'true') { - return false; - } else { - return true; - } - } - - private function GetCommunityContainers() : string { - $envVariableName = 'AIO_COMMUNITY_CONTAINERS'; - $configName = 'aio_community_containers'; - $defaultValue = ''; - return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); - } - - public function GetEnabledCommunityContainers() : array { - return explode(' ', $this->GetCommunityContainers()); - } - - private function GetEnabledDriDevice() : string { - $envVariableName = 'NEXTCLOUD_ENABLE_DRI_DEVICE'; - $configName = 'nextcloud_enable_dri_device'; - $defaultValue = ''; - return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); - } - - public function isDriDeviceEnabled() : bool { - if ($this->GetEnabledDriDevice() === 'true') { - return true; - } else { - return false; - } - } - - private function GetKeepDisabledApps() : string { - $envVariableName = 'NEXTCLOUD_KEEP_DISABLED_APPS'; - $configName = 'nextcloud_keep_disabled_apps'; - $defaultValue = ''; - return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); - } - - public function shouldDisabledAppsGetRemoved() : bool { - if ($this->GetKeepDisabledApps() === 'true') { - return false; - } else { - return true; - } - } +class ConfigurationManager { + public function GetConfig() : array { + if (file_exists(DataConst::GetConfigFile())) { + $configContent = file_get_contents(DataConst::GetConfigFile()); + return json_decode($configContent, true, 512, JSON_THROW_ON_ERROR); + } + + return []; + } + + public function GetPassword() : string { + return $this->GetConfig()['password']; + } + + public function GetToken() : string { + return $this->GetConfig()['AIO_TOKEN']; + } + + public function SetPassword(string $password) : void { + $config = $this->GetConfig(); + $config['password'] = $password; + $this->WriteConfig($config); + } + + public function GetAndGenerateSecret(string $secretId) : string { + $config = $this->GetConfig(); + if (!isset($config['secrets'][$secretId])) { + $config['secrets'][$secretId] = bin2hex(random_bytes(24)); + $this->WriteConfig($config); + } + + if ($secretId === 'BORGBACKUP_PASSWORD' && !file_exists(DataConst::GetBackupSecretFile())) { + $this->DoubleSafeBackupSecret($config['secrets'][$secretId]); + } + + return $config['secrets'][$secretId]; + } + + public function GetSecret(string $secretId) : string { + $config = $this->GetConfig(); + if (!isset($config['secrets'][$secretId])) { + $config['secrets'][$secretId] = ''; + } + + return $config['secrets'][$secretId]; + } + + private function DoubleSafeBackupSecret(string $borgBackupPassword) : void { + file_put_contents(DataConst::GetBackupSecretFile(), $borgBackupPassword); + } + + public function hasBackupRunOnce() : bool { + if (!file_exists(DataConst::GetBackupKeyFile())) { + return false; + } else { + return true; + } + } + + public function GetLastBackupTime() : string { + if (!file_exists(DataConst::GetBackupArchivesList())) { + return ''; + } + + $content = file_get_contents(DataConst::GetBackupArchivesList()); + if ($content === '') { + return ''; + } + + $lastBackupLines = explode("\n", $content); + $lastBackupLine = ''; + if (count($lastBackupLines) >= 2) { + $lastBackupLine = $lastBackupLines[sizeof($lastBackupLines) - 2]; + } + if ($lastBackupLine === '') { + return ''; + } + + $lastBackupTimes = explode(',', $lastBackupLine); + $lastBackupTime = $lastBackupTimes[1]; + if ($lastBackupTime === '') { + return ''; + } + + return $lastBackupTime; + } + + public function GetBackupTimes() : array { + if (!file_exists(DataConst::GetBackupArchivesList())) { + return []; + } + + $content = file_get_contents(DataConst::GetBackupArchivesList()); + if ($content === '') { + return []; + } + + $backupLines = explode("\n", $content); + $backupTimes = []; + foreach ($backupLines as $lines) { + if ($lines !== '') { + $backupTimesTemp = explode(',', $lines); + $backupTimes[] = $backupTimesTemp[1]; + } + } + + // Reverse the array to list newest backup first + $backupTimes = array_reverse($backupTimes); + + return $backupTimes; + } + + public function wasStartButtonClicked() : bool { + if (isset($this->GetConfig()['wasStartButtonClicked'])) { + return true; + } else { + return false; + } + } + + public function isx64Platform() : bool { + if (php_uname('m') === 'x86_64') { + return true; + } else { + return false; + } + } + + public function isClamavEnabled() : bool { + if (!$this->isx64Platform()) { + return false; + } + + $config = $this->GetConfig(); + if (isset($config['isClamavEnabled']) && $config['isClamavEnabled'] === 1) { + return true; + } else { + return false; + } + } + + public function isDockerSocketProxyEnabled() : bool { + $config = $this->GetConfig(); + if (isset($config['isDockerSocketProxyEnabled']) && $config['isDockerSocketProxyEnabled'] === 1) { + return true; + } else { + return false; + } + } + + public function SetDockerSocketProxyEnabledState(int $value) : void { + $config = $this->GetConfig(); + $config['isDockerSocketProxyEnabled'] = $value; + $this->WriteConfig($config); + } + + public function isWhiteboardEnabled() : bool { + $config = $this->GetConfig(); + if (isset($config['isWhiteboardEnabled']) && $config['isWhiteboardEnabled'] === 1) { + return true; + } else { + return false; + } + } + + public function SetWhiteboardEnabledState(int $value) : void { + $config = $this->GetConfig(); + $config['isWhiteboardEnabled'] = $value; + $this->WriteConfig($config); + } + + public function SetClamavEnabledState(int $value) : void { + $config = $this->GetConfig(); + $config['isClamavEnabled'] = $value; + $this->WriteConfig($config); + } + + public function isImaginaryEnabled() : bool { + $config = $this->GetConfig(); + if (isset($config['isImaginaryEnabled']) && $config['isImaginaryEnabled'] === 0) { + return false; + } else { + return true; + } + } + + public function SetImaginaryEnabledState(int $value) : void { + $config = $this->GetConfig(); + $config['isImaginaryEnabled'] = $value; + $this->WriteConfig($config); + } + + public function isFulltextsearchEnabled() : bool { + $config = $this->GetConfig(); + if (isset($config['isFulltextsearchEnabled']) && $config['isFulltextsearchEnabled'] === 1) { + return true; + } else { + return false; + } + } + + public function SetFulltextsearchEnabledState(int $value) : void { + $config = $this->GetConfig(); + $config['isFulltextsearchEnabled'] = $value; + $this->WriteConfig($config); + } + + public function isOnlyofficeEnabled() : bool { + $config = $this->GetConfig(); + if (isset($config['isOnlyofficeEnabled']) && $config['isOnlyofficeEnabled'] === 1) { + return true; + } else { + return false; + } + } + + public function SetOnlyofficeEnabledState(int $value) : void { + $config = $this->GetConfig(); + $config['isOnlyofficeEnabled'] = $value; + $this->WriteConfig($config); + } + + public function isCollaboraEnabled() : bool { + $config = $this->GetConfig(); + if (isset($config['isCollaboraEnabled']) && $config['isCollaboraEnabled'] === 0) { + return false; + } else { + return true; + } + } + + public function SetCollaboraEnabledState(int $value) : void { + $config = $this->GetConfig(); + $config['isCollaboraEnabled'] = $value; + $this->WriteConfig($config); + } + + public function isTalkEnabled() : bool { + $config = $this->GetConfig(); + if (isset($config['isTalkEnabled']) && $config['isTalkEnabled'] === 0) { + return false; + } else { + return true; + } + } + + public function SetTalkEnabledState(int $value) : void { + $config = $this->GetConfig(); + $config['isTalkEnabled'] = $value; + $this->WriteConfig($config); + } + + public function isTalkRecordingEnabled() : bool { + if (!$this->isTalkEnabled()) { + return false; + } + $config = $this->GetConfig(); + if (isset($config['isTalkRecordingEnabled']) && $config['isTalkRecordingEnabled'] === 1) { + return true; + } else { + return false; + } + } + + public function SetTalkRecordingEnabledState(int $value) : void { + if (!$this->isTalkEnabled()) { + $value = 0; + } + $config = $this->GetConfig(); + $config['isTalkRecordingEnabled'] = $value; + $this->WriteConfig($config); + } + + /** + * @throws InvalidSettingConfigurationException + */ + public function SetDomain(string $domain) : void { + // Validate that at least one dot is contained + if (!str_contains($domain, '.')) { + throw new InvalidSettingConfigurationException('Domain must contain at least one dot!'); + } + + // Validate that no slashes are contained + if (str_contains($domain, '/')) { + throw new InvalidSettingConfigurationException('Domain must not contain slashes!'); + } + + // Validate that no colons are contained + if (str_contains($domain, ':')) { + throw new InvalidSettingConfigurationException('Domain must not contain colons!'); + } + + // Validate domain + if (filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) === false) { + throw new InvalidSettingConfigurationException('Domain is not a valid domain!'); + } + + // Validate that it is not an IP-address + if (filter_var($domain, FILTER_VALIDATE_IP)) { + throw new InvalidSettingConfigurationException('Please enter a domain and not an IP-address!'); + } + + // Skip domain validation if opted in to do so + if (!$this->shouldDomainValidationBeSkipped()) { + + $dnsRecordIP = gethostbyname($domain); + if ($dnsRecordIP === $domain) { + $dnsRecordIP = ''; + } + + if (empty($dnsRecordIP)) { + $record = dns_get_record($domain, DNS_AAAA); + if (isset($record[0]['ipv6']) && !empty($record[0]['ipv6'])) { + $dnsRecordIP = $record[0]['ipv6']; + } + } + + // Validate IP + if (!filter_var($dnsRecordIP, FILTER_VALIDATE_IP)) { + throw new InvalidSettingConfigurationException("DNS config is not set for this domain or the domain is not a valid domain! (It was found to be set to '" . $dnsRecordIP . "')"); + } + + // Get the apache port + $port = $this->GetApachePort(); + + if (!filter_var($dnsRecordIP, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { + if ($port === '443') { + throw new InvalidSettingConfigurationException("It seems like the ip-address of the domain is set to an internal or reserved ip-address. This is not supported. (It was found to be set to '" . $dnsRecordIP . "'). Please set it to a public ip-address so that the domain validation can work!"); + } else { + error_log('It seems like the ip-address of ' . $domain . " is set to an internal or reserved ip-address. (It was found to be set to '" . $dnsRecordIP . "')"); + } + } + + // Check if port 443 is open + $connection = @fsockopen($domain, 443, $errno, $errstr, 10); + if ($connection) { + fclose($connection); + } else { + throw new InvalidSettingConfigurationException('The domain is not reachable on Port 443 from within this container. Have you opened port 443/tcp in your router/firewall? If yes is the problem most likely that the router or firewall forbids local access to your domain. You can work around that by setting up a local DNS-server.'); + } + + // Get Instance ID + $instanceID = $this->GetAndGenerateSecret('INSTANCE_ID'); + + // set protocol + if ($port !== '443') { + $protocol = 'https://'; + } else { + $protocol = 'http://'; + } + + // Check if response is correct + $ch = curl_init(); + $testUrl = $protocol . $domain . ':443'; + curl_setopt($ch, CURLOPT_URL, $testUrl); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + $response = (string)curl_exec($ch); + # Get rid of trailing \n + $response = str_replace("\n", '', $response); + + if ($response !== $instanceID) { + error_log('The response of the connection attempt to "' . $testUrl . '" was: ' . $response); + error_log('Expected was: ' . $instanceID); + error_log('The error message was: ' . curl_error($ch)); + $notice = "Domain does not point to this server or the reverse proxy is not configured correctly. See the mastercontainer logs for more details. ('sudo docker logs -f nextcloud-aio-mastercontainer')"; + if ($port === '443') { + $notice .= ' If you should be using Cloudflare, make sure to disable the Cloudflare Proxy feature as it might block the domain validation. Same for any other firewall or service that blocks unencrypted access on port 443.'; + } else { + error_log('Please follow https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md#6-how-to-debug-things in order to debug things!'); + } + throw new InvalidSettingConfigurationException($notice); + } + } + + // Write domain + $config = $this->GetConfig(); + $config['domain'] = $domain; + // Reset the borg restore password when setting the domain + $config['borg_restore_password'] = ''; + $this->WriteConfig($config); + } + + public function GetDomain() : string { + $config = $this->GetConfig(); + if (!isset($config['domain'])) { + $config['domain'] = ''; + } + + return $config['domain']; + } + + public function GetBaseDN() : string { + $domain = $this->GetDomain(); + if ($domain === '') { + return ''; + } + return 'dc=' . implode(',dc=', explode('.', $domain)); + } + + public function GetBackupMode() : string { + $config = $this->GetConfig(); + if (!isset($config['backup-mode'])) { + $config['backup-mode'] = ''; + } + + return $config['backup-mode']; + } + + public function GetSelectedRestoreTime() : string { + $config = $this->GetConfig(); + if (!isset($config['selected-restore-time'])) { + $config['selected-restore-time'] = ''; + } + + return $config['selected-restore-time']; + } + + public function GetAIOURL() : string { + $config = $this->GetConfig(); + if (!isset($config['AIO_URL'])) { + $config['AIO_URL'] = ''; + } + + return $config['AIO_URL']; + } + + /** + * @throws InvalidSettingConfigurationException + */ + public function SetBorgBackupHostLocation(string $location) : void { + $isValidPath = false; + if (str_starts_with($location, '/') && !str_ends_with($location, '/')) { + $isValidPath = true; + } elseif ($location === 'nextcloud_aio_backupdir') { + $isValidPath = true; + } + + if (!$isValidPath) { + throw new InvalidSettingConfigurationException("The path must start with '/', and must not end with '/'!"); + } + + + $config = $this->GetConfig(); + $config['borg_backup_host_location'] = $location; + $this->WriteConfig($config); + } + + public function DeleteBorgBackupHostLocation() : void { + $config = $this->GetConfig(); + $config['borg_backup_host_location'] = ''; + $this->WriteConfig($config); + } + + /** + * @throws InvalidSettingConfigurationException + */ + public function SetBorgRestoreHostLocationAndPassword(string $location, string $password) : void { + if ($location === '') { + throw new InvalidSettingConfigurationException('Please enter a path!'); + } + + $isValidPath = false; + if (str_starts_with($location, '/') && !str_ends_with($location, '/')) { + $isValidPath = true; + } elseif ($location === 'nextcloud_aio_backupdir') { + $isValidPath = true; + } + + if (!$isValidPath) { + throw new InvalidSettingConfigurationException("The path must start with '/', and must not end with '/'!"); + } + + if ($password === '') { + throw new InvalidSettingConfigurationException('Please enter the password!'); + } + + $config = $this->GetConfig(); + $config['borg_backup_host_location'] = $location; + $config['borg_restore_password'] = $password; + $config['instance_restore_attempt'] = 1; + $this->WriteConfig($config); + } + + /** + * @throws InvalidSettingConfigurationException + */ + public function ChangeMasterPassword(string $currentPassword, string $newPassword) : void { + if ($currentPassword === '') { + throw new InvalidSettingConfigurationException('Please enter your current password.'); + } + + if ($currentPassword !== $this->GetPassword()) { + throw new InvalidSettingConfigurationException('The entered current password is not correct.'); + } + + if ($newPassword === '') { + throw new InvalidSettingConfigurationException('Please enter a new password.'); + } + + if (strlen($newPassword) < 24) { + throw new InvalidSettingConfigurationException('New passwords must be >= 24 digits.'); + } + + if (!preg_match('#^[a-zA-Z0-9 ]+$#', $newPassword)) { + throw new InvalidSettingConfigurationException('Not allowed characters in the new password.'); + } + + // All checks pass so set the password + $this->SetPassword($newPassword); + } + + public function GetApachePort() : string { + $envVariableName = 'APACHE_PORT'; + $configName = 'apache_port'; + $defaultValue = '443'; + return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); + } + + public function GetTalkPort() : string { + $envVariableName = 'TALK_PORT'; + $configName = 'talk_port'; + $defaultValue = '3478'; + return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); + } + + /** + * @throws InvalidSettingConfigurationException + */ + public function WriteConfig(array $config) : void { + if (!is_dir(DataConst::GetDataDirectory())) { + throw new InvalidSettingConfigurationException(DataConst::GetDataDirectory() . ' does not exist! Something was set up falsely!'); + } + $df = disk_free_space(DataConst::GetDataDirectory()); + $content = json_encode($config, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR); + $size = strlen($content) + 10240; + if ($df !== false && (int)$df < $size) { + throw new InvalidSettingConfigurationException(DataConst::GetDataDirectory() . ' does not have enough space for writing the config file! Not writing it back!'); + } + file_put_contents(DataConst::GetConfigFile(), $content); + } + + private function GetEnvironmentalVariableOrConfig(string $envVariableName, string $configName, string $defaultValue) : string { + $envVariableOutput = getenv($envVariableName); + if ($envVariableOutput === false) { + $config = $this->GetConfig(); + if (!isset($config[$configName]) || $config[$configName] === '') { + $config[$configName] = $defaultValue; + } + return $config[$configName]; + } + if (file_exists(DataConst::GetConfigFile())) { + $config = $this->GetConfig(); + if (!isset($config[$configName])) { + $config[$configName] = ''; + } + if ($envVariableOutput !== $config[$configName]) { + $config[$configName] = $envVariableOutput; + $this->WriteConfig($config); + } + } + return $envVariableOutput; + } + + public function GetBorgBackupHostLocation() : string { + $config = $this->GetConfig(); + if (!isset($config['borg_backup_host_location'])) { + $config['borg_backup_host_location'] = ''; + } + + return $config['borg_backup_host_location']; + } + + public function GetBorgRestorePassword() : string { + $config = $this->GetConfig(); + if (!isset($config['borg_restore_password'])) { + $config['borg_restore_password'] = ''; + } + + return $config['borg_restore_password']; + } + + public function isInstanceRestoreAttempt() : bool { + $config = $this->GetConfig(); + if (!isset($config['instance_restore_attempt'])) { + $config['instance_restore_attempt'] = ''; + } + + if ($config['instance_restore_attempt'] === 1) { + return true; + } + return false; + } + + public function GetBorgBackupMode() : string { + $config = $this->GetConfig(); + if (!isset($config['backup-mode'])) { + $config['backup-mode'] = ''; + } + + return $config['backup-mode']; + } + + public function GetNextcloudMount() : string { + $envVariableName = 'NEXTCLOUD_MOUNT'; + $configName = 'nextcloud_mount'; + $defaultValue = ''; + return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); + } + + public function GetNextcloudDatadirMount() : string { + $envVariableName = 'NEXTCLOUD_DATADIR'; + $configName = 'nextcloud_datadir'; + $defaultValue = 'nextcloud_aio_nextcloud_data'; + return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); + } + + public function GetNextcloudUploadLimit() : string { + $envVariableName = 'NEXTCLOUD_UPLOAD_LIMIT'; + $configName = 'nextcloud_upload_limit'; + $defaultValue = '10G'; + return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); + } + + public function GetNextcloudMemoryLimit() : string { + $envVariableName = 'NEXTCLOUD_MEMORY_LIMIT'; + $configName = 'nextcloud_memory_limit'; + $defaultValue = '512M'; + return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); + } + + public function GetApacheMaxSize() : int { + $uploadLimit = (int)rtrim($this->GetNextcloudUploadLimit(), 'G'); + return $uploadLimit * 1024 * 1024 * 1024; + } + + public function GetNextcloudMaxTime() : string { + $envVariableName = 'NEXTCLOUD_MAX_TIME'; + $configName = 'nextcloud_max_time'; + $defaultValue = '3600'; + return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); + } + + public function GetBorgRetentionPolicy() : string { + $envVariableName = 'BORG_RETENTION_POLICY'; + $configName = 'borg_retention_policy'; + $defaultValue = '--keep-within=7d --keep-weekly=4 --keep-monthly=6'; + return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); + } + + public function GetDockerSocketPath() : string { + $envVariableName = 'WATCHTOWER_DOCKER_SOCKET_PATH'; + $configName = 'docker_socket_path'; + $defaultValue = '/var/run/docker.sock'; + return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); + } + + public function GetTrustedCacertsDir() : string { + $envVariableName = 'NEXTCLOUD_TRUSTED_CACERTS_DIR'; + $configName = 'trusted_cacerts_dir'; + $defaultValue = ''; + return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); + } + + public function GetNextcloudAdditionalApks() : string { + $envVariableName = 'NEXTCLOUD_ADDITIONAL_APKS'; + $configName = 'nextcloud_additional_apks'; + $defaultValue = 'imagemagick'; + return trim($this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue)); + } + + public function GetNextcloudAdditionalPhpExtensions() : string { + $envVariableName = 'NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS'; + $configName = 'nextcloud_additional_php_extensions'; + $defaultValue = 'imagick'; + return trim($this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue)); + } + + public function GetCollaboraSeccompPolicy() : string { + $defaultString = '--o:security.seccomp='; + if ($this->GetCollaboraSeccompDisabledState() !== 'true') { + return $defaultString . 'true'; + } + return $defaultString . 'false'; + } + + private function GetCollaboraSeccompDisabledState() : string { + $envVariableName = 'COLLABORA_SECCOMP_DISABLED'; + $configName = 'collabora_seccomp_disabled'; + $defaultValue = 'false'; + return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); + } + + /** + * @throws InvalidSettingConfigurationException + */ + public function SetDailyBackupTime(string $time, bool $enableAutomaticUpdates, bool $successNotification) : void { + if ($time === '') { + throw new InvalidSettingConfigurationException('The daily backup time must not be empty!'); + } + + if (!preg_match('#^[0-1][0-9]:[0-5][0-9]$#', $time) && !preg_match('#^2[0-3]:[0-5][0-9]$#', $time)) { + throw new InvalidSettingConfigurationException("You did not enter a correct time! One correct example is '04:00'!"); + } + + if ($enableAutomaticUpdates === false) { + $time .= PHP_EOL . 'automaticUpdatesAreNotEnabled'; + } else { + $time .= PHP_EOL; + } + if ($successNotification === false) { + $time .= PHP_EOL . 'successNotificationsAreNotEnabled'; + } else { + $time .= PHP_EOL; + } + file_put_contents(DataConst::GetDailyBackupTimeFile(), $time); + } + + public function GetDailyBackupTime() : string { + if (!file_exists(DataConst::GetDailyBackupTimeFile())) { + return ''; + } + $dailyBackupFile = file_get_contents(DataConst::GetDailyBackupTimeFile()); + $dailyBackupFileArray = explode("\n", $dailyBackupFile); + return $dailyBackupFileArray[0]; + } + + public function areAutomaticUpdatesEnabled() : bool { + if (!file_exists(DataConst::GetDailyBackupTimeFile())) { + return false; + } + $dailyBackupFile = file_get_contents(DataConst::GetDailyBackupTimeFile()); + $dailyBackupFileArray = explode("\n", $dailyBackupFile); + if (isset($dailyBackupFileArray[1]) && $dailyBackupFileArray[1] === 'automaticUpdatesAreNotEnabled') { + return false; + } else { + return true; + } + } + + public function DeleteDailyBackupTime() : void { + if (file_exists(DataConst::GetDailyBackupTimeFile())) { + unlink(DataConst::GetDailyBackupTimeFile()); + } + } + + /** + * @throws InvalidSettingConfigurationException + */ + public function SetAdditionalBackupDirectories(string $additionalBackupDirectories) : void { + $additionalBackupDirectoriesArray = explode("\n", $additionalBackupDirectories); + $validDirectories = ''; + foreach ($additionalBackupDirectoriesArray as $entry) { + // Trim all unwanted chars on both sites + $entry = trim($entry); + if ($entry !== '') { + if (!preg_match('#^/[.0-9a-zA-Z/_-]+$#', $entry) && !preg_match('#^[.0-9a-zA-Z_-]+$#', $entry)) { + throw new InvalidSettingConfigurationException('You entered unallowed characters! Problematic is ' . $entry); + } + $validDirectories .= rtrim($entry, '/') . PHP_EOL; + } + } + + if ($validDirectories === '') { + unlink(DataConst::GetAdditionalBackupDirectoriesFile()); + } else { + file_put_contents(DataConst::GetAdditionalBackupDirectoriesFile(), $validDirectories); + } + } + + public function shouldLatestMajorGetInstalled() : bool { + $config = $this->GetConfig(); + if (!isset($config['install_latest_major'])) { + $config['install_latest_major'] = ''; + } + return $config['install_latest_major'] !== ''; + } + + public function GetAdditionalBackupDirectoriesString() : string { + if (!file_exists(DataConst::GetAdditionalBackupDirectoriesFile())) { + return ''; + } + $additionalBackupDirectories = file_get_contents(DataConst::GetAdditionalBackupDirectoriesFile()); + return $additionalBackupDirectories; + } + + public function GetAdditionalBackupDirectoriesArray() : array { + $additionalBackupDirectories = $this->GetAdditionalBackupDirectoriesString(); + $additionalBackupDirectoriesArray = explode("\n", $additionalBackupDirectories); + $additionalBackupDirectoriesArray = array_unique($additionalBackupDirectoriesArray, SORT_REGULAR); + return $additionalBackupDirectoriesArray; + } + + public function isDailyBackupRunning() : bool { + if (file_exists(DataConst::GetDailyBackupBlockFile())) { + return true; + } + return false; + } + + public function GetTimezone() : string { + $config = $this->GetConfig(); + if (!isset($config['timezone'])) { + $config['timezone'] = ''; + } + + return $config['timezone']; + } + + /** + * @throws InvalidSettingConfigurationException + */ + public function SetTimezone(string $timezone) : void { + if ($timezone === '') { + throw new InvalidSettingConfigurationException('The timezone must not be empty!'); + } + + if (!preg_match("#^[a-zA-Z0-9_\-\/\+]+$#", $timezone)) { + throw new InvalidSettingConfigurationException('The entered timezone does not seem to be a valid timezone!'); + } + + $config = $this->GetConfig(); + $config['timezone'] = $timezone; + $this->WriteConfig($config); + } + + public function DeleteTimezone() : void { + $config = $this->GetConfig(); + $config['timezone'] = ''; + $this->WriteConfig($config); + } + + public function shouldDomainValidationBeSkipped() : bool { + if (getenv('SKIP_DOMAIN_VALIDATION') !== false) { + return true; + } + return false; + } + + public function GetNextcloudStartupApps() : string { + $apps = getenv('NEXTCLOUD_STARTUP_APPS'); + if (is_string($apps)) { + return trim($apps); + } + return 'deck twofactor_totp tasks calendar contacts notes'; + } + + public function GetCollaboraDictionaries() : string { + $config = $this->GetConfig(); + if (!isset($config['collabora_dictionaries'])) { + $config['collabora_dictionaries'] = ''; + } + + return $config['collabora_dictionaries']; + } + + /** + * @throws InvalidSettingConfigurationException + */ + public function SetCollaboraDictionaries(string $CollaboraDictionaries) : void { + if ($CollaboraDictionaries === '') { + throw new InvalidSettingConfigurationException('The dictionaries must not be empty!'); + } + + if (!preg_match('#^[a-zA-Z_ ]+$#', $CollaboraDictionaries)) { + throw new InvalidSettingConfigurationException('The entered dictionaries do not seem to be a valid!'); + } + + $config = $this->GetConfig(); + $config['collabora_dictionaries'] = $CollaboraDictionaries; + $this->WriteConfig($config); + } + + public function DeleteCollaboraDictionaries() : void { + $config = $this->GetConfig(); + $config['collabora_dictionaries'] = ''; + $this->WriteConfig($config); + } + + public function GetApacheIPBinding() : string { + $envVariableName = 'APACHE_IP_BINDING'; + $configName = 'apache_ip_binding'; + $defaultValue = ''; + return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); + } + + private function GetDisableBackupSection() : string { + $envVariableName = 'AIO_DISABLE_BACKUP_SECTION'; + $configName = 'disable_backup_section'; + $defaultValue = ''; + return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); + } + + public function isBackupSectionEnabled() : bool { + if ($this->GetDisableBackupSection() === 'true') { + return false; + } else { + return true; + } + } + + private function GetCommunityContainers() : string { + $envVariableName = 'AIO_COMMUNITY_CONTAINERS'; + $configName = 'aio_community_containers'; + $defaultValue = ''; + return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); + } + + public function GetEnabledCommunityContainers() : array { + return explode(' ', $this->GetCommunityContainers()); + } + + private function GetEnabledDriDevice() : string { + $envVariableName = 'NEXTCLOUD_ENABLE_DRI_DEVICE'; + $configName = 'nextcloud_enable_dri_device'; + $defaultValue = ''; + return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); + } + + public function isDriDeviceEnabled() : bool { + if ($this->GetEnabledDriDevice() === 'true') { + return true; + } else { + return false; + } + } + + private function GetKeepDisabledApps() : string { + $envVariableName = 'NEXTCLOUD_KEEP_DISABLED_APPS'; + $configName = 'nextcloud_keep_disabled_apps'; + $defaultValue = ''; + return $this->GetEnvironmentalVariableOrConfig($envVariableName, $configName, $defaultValue); + } + + public function shouldDisabledAppsGetRemoved() : bool { + if ($this->GetKeepDisabledApps() === 'true') { + return false; + } else { + return true; + } + } } diff --git a/php/src/Data/DataConst.php b/php/src/Data/DataConst.php index 2512b7fb679..cae29dca340 100644 --- a/php/src/Data/DataConst.php +++ b/php/src/Data/DataConst.php @@ -3,55 +3,55 @@ namespace AIO\Data; class DataConst { - public static function GetDataDirectory() : string { - if(is_dir('/mnt/docker-aio-config/data/')) { - return '/mnt/docker-aio-config/data/'; - } + public static function GetDataDirectory() : string { + if (is_dir('/mnt/docker-aio-config/data/')) { + return '/mnt/docker-aio-config/data/'; + } - return realpath(__DIR__ . '/../../data/'); - } + return realpath(__DIR__ . '/../../data/'); + } - public static function GetSessionDirectory() : string { - if(is_dir('/mnt/docker-aio-config/session/')) { - return '/mnt/docker-aio-config/session/'; - } + public static function GetSessionDirectory() : string { + if (is_dir('/mnt/docker-aio-config/session/')) { + return '/mnt/docker-aio-config/session/'; + } - return realpath(__DIR__ . '/../../session/'); - } + return realpath(__DIR__ . '/../../session/'); + } - public static function GetConfigFile() : string { - return self::GetDataDirectory() . '/configuration.json'; - } + public static function GetConfigFile() : string { + return self::GetDataDirectory() . '/configuration.json'; + } - public static function GetBackupSecretFile() : string { - return self::GetDataDirectory() . '/backupsecret'; - } + public static function GetBackupSecretFile() : string { + return self::GetDataDirectory() . '/backupsecret'; + } - public static function GetDailyBackupTimeFile() : string { - return self::GetDataDirectory() . '/daily_backup_time'; - } + public static function GetDailyBackupTimeFile() : string { + return self::GetDataDirectory() . '/daily_backup_time'; + } - public static function GetAdditionalBackupDirectoriesFile() : string { - return self::GetDataDirectory() . '/additional_backup_directories'; - } + public static function GetAdditionalBackupDirectoriesFile() : string { + return self::GetDataDirectory() . '/additional_backup_directories'; + } - public static function GetDailyBackupBlockFile() : string { - return self::GetDataDirectory() . '/daily_backup_running'; - } + public static function GetDailyBackupBlockFile() : string { + return self::GetDataDirectory() . '/daily_backup_running'; + } - public static function GetBackupKeyFile() : string { - return self::GetDataDirectory() . '/borg.config'; - } + public static function GetBackupKeyFile() : string { + return self::GetDataDirectory() . '/borg.config'; + } - public static function GetBackupArchivesList() : string { - return self::GetDataDirectory() . '/backup_archives.list'; - } + public static function GetBackupArchivesList() : string { + return self::GetDataDirectory() . '/backup_archives.list'; + } - public static function GetSessionDateFile() : string { - return self::GetDataDirectory() . '/session_date_file'; - } + public static function GetSessionDateFile() : string { + return self::GetDataDirectory() . '/session_date_file'; + } - public static function GetCommunityContainersDirectory() : string { - return realpath(__DIR__ . '/../../../community-containers/'); - } + public static function GetCommunityContainersDirectory() : string { + return realpath(__DIR__ . '/../../../community-containers/'); + } } diff --git a/php/src/Data/InvalidSettingConfigurationException.php b/php/src/Data/InvalidSettingConfigurationException.php index 9db74adde7a..25eede10dc9 100644 --- a/php/src/Data/InvalidSettingConfigurationException.php +++ b/php/src/Data/InvalidSettingConfigurationException.php @@ -3,4 +3,5 @@ declare(strict_types=1); namespace AIO\Data; -class InvalidSettingConfigurationException extends \Exception {} +class InvalidSettingConfigurationException extends \Exception { +} diff --git a/php/src/Data/Setup.php b/php/src/Data/Setup.php index 2ab87e36e85..e5d0ff69229 100644 --- a/php/src/Data/Setup.php +++ b/php/src/Data/Setup.php @@ -4,29 +4,28 @@ use AIO\Auth\PasswordGenerator; -class Setup -{ - private PasswordGenerator $passwordGenerator; - private ConfigurationManager $configurationManager; +class Setup { + private PasswordGenerator $passwordGenerator; + private ConfigurationManager $configurationManager; - public function __construct( - PasswordGenerator $passwordGenerator, - ConfigurationManager $configurationManager) { - $this->passwordGenerator = $passwordGenerator; - $this->configurationManager = $configurationManager; - } + public function __construct( + PasswordGenerator $passwordGenerator, + ConfigurationManager $configurationManager) { + $this->passwordGenerator = $passwordGenerator; + $this->configurationManager = $configurationManager; + } - public function Setup() : string { - if(!$this->CanBeInstalled()) { - return ''; - } + public function Setup() : string { + if (!$this->CanBeInstalled()) { + return ''; + } - $password = $this->passwordGenerator->GeneratePassword(8); - $this->configurationManager->SetPassword($password); - return $password; - } + $password = $this->passwordGenerator->GeneratePassword(8); + $this->configurationManager->SetPassword($password); + return $password; + } - public function CanBeInstalled() : bool { - return !file_exists(DataConst::GetConfigFile()); - } + public function CanBeInstalled() : bool { + return !file_exists(DataConst::GetConfigFile()); + } } diff --git a/php/src/DependencyInjection.php b/php/src/DependencyInjection.php index e37a0917339..be247370aaa 100644 --- a/php/src/DependencyInjection.php +++ b/php/src/DependencyInjection.php @@ -5,44 +5,43 @@ use AIO\Docker\DockerHubManager; use DI\Container; -class DependencyInjection -{ - public static function GetContainer() : Container { - $container = new Container(); +class DependencyInjection { + public static function GetContainer() : Container { + $container = new Container(); - $container->set( - DockerHubManager::class, - new DockerHubManager() - ); + $container->set( + DockerHubManager::class, + new DockerHubManager() + ); - $container->set( - \AIO\Data\ConfigurationManager::class, - new \AIO\Data\ConfigurationManager() - ); - $container->set( - \AIO\Docker\DockerActionManager::class, - new \AIO\Docker\DockerActionManager( - $container->get(\AIO\Data\ConfigurationManager::class), - $container->get(\AIO\ContainerDefinitionFetcher::class), - $container->get(DockerHubManager::class) - ) - ); - $container->set( - \AIO\Auth\PasswordGenerator::class, - new \AIO\Auth\PasswordGenerator() - ); - $container->set( - \AIO\Auth\AuthManager::class, - new \AIO\Auth\AuthManager($container->get(\AIO\Data\ConfigurationManager::class)) - ); - $container->set( - \AIO\Data\Setup::class, - new \AIO\Data\Setup( - $container->get(\AIO\Auth\PasswordGenerator::class), - $container->get(\AIO\Data\ConfigurationManager::class) - ) - ); + $container->set( + \AIO\Data\ConfigurationManager::class, + new \AIO\Data\ConfigurationManager() + ); + $container->set( + \AIO\Docker\DockerActionManager::class, + new \AIO\Docker\DockerActionManager( + $container->get(\AIO\Data\ConfigurationManager::class), + $container->get(\AIO\ContainerDefinitionFetcher::class), + $container->get(DockerHubManager::class) + ) + ); + $container->set( + \AIO\Auth\PasswordGenerator::class, + new \AIO\Auth\PasswordGenerator() + ); + $container->set( + \AIO\Auth\AuthManager::class, + new \AIO\Auth\AuthManager($container->get(\AIO\Data\ConfigurationManager::class)) + ); + $container->set( + \AIO\Data\Setup::class, + new \AIO\Data\Setup( + $container->get(\AIO\Auth\PasswordGenerator::class), + $container->get(\AIO\Data\ConfigurationManager::class) + ) + ); - return $container; - } -} \ No newline at end of file + return $container; + } +} diff --git a/php/src/Docker/DockerActionManager.php b/php/src/Docker/DockerActionManager.php index 6bb7f70ee75..137d56196c4 100644 --- a/php/src/Docker/DockerActionManager.php +++ b/php/src/Docker/DockerActionManager.php @@ -5,1031 +5,1010 @@ use AIO\Container\Container; use AIO\Container\State\IContainerState; use AIO\Container\State\ImageDoesNotExistState; -use AIO\Container\State\StartingState; -use AIO\Container\State\RunningState; -use AIO\Container\State\RestartingState; use AIO\Container\State\NotRestartingState; -use AIO\Container\State\VersionDifferentState; +use AIO\Container\State\RestartingState; +use AIO\Container\State\RunningState; +use AIO\Container\State\StartingState; use AIO\Container\State\StoppedState; +use AIO\Container\State\VersionDifferentState; use AIO\Container\State\VersionEqualState; +use AIO\ContainerDefinitionFetcher; use AIO\Data\ConfigurationManager; use GuzzleHttp\Exception\RequestException; -use AIO\ContainerDefinitionFetcher; -use http\Env\Response; - -class DockerActionManager -{ - private const string API_VERSION = 'v1.41'; - private \GuzzleHttp\Client $guzzleClient; - private ConfigurationManager $configurationManager; - private ContainerDefinitionFetcher $containerDefinitionFetcher; - private DockerHubManager $dockerHubManager; - - public function __construct( - ConfigurationManager $configurationManager, - ContainerDefinitionFetcher $containerDefinitionFetcher, - DockerHubManager $dockerHubManager - ) { - $this->configurationManager = $configurationManager; - $this->containerDefinitionFetcher = $containerDefinitionFetcher; - $this->dockerHubManager = $dockerHubManager; - $this->guzzleClient = new \GuzzleHttp\Client( - [ - 'curl' => [ - CURLOPT_UNIX_SOCKET_PATH => '/var/run/docker.sock', - - ], - ] - ); - } - - private function BuildApiUrl(string $url) : string { - return sprintf('http://127.0.0.1/%s/%s', self::API_VERSION, $url); - } - - private function BuildImageName(Container $container) : string { - $tag = $container->GetImageTag(); - if ($tag === '%AIO_CHANNEL%') { - $tag = $this->GetCurrentChannel(); - } - return $container->GetContainerName() . ':' . $tag; - } - - public function GetContainerRunningState(Container $container) : IContainerState - { - $url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($container->GetIdentifier()))); - try { - $response = $this->guzzleClient->get($url); - } catch (RequestException $e) { - if ($e->getCode() === 404) { - return new ImageDoesNotExistState(); - } - throw $e; - } - - $responseBody = json_decode((string)$response->getBody(), true); - - if ($responseBody['State']['Running'] === true) { - return new RunningState(); - } else { - return new StoppedState(); - } - } - - public function GetContainerRestartingState(Container $container) : IContainerState - { - $url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($container->GetIdentifier()))); - try { - $response = $this->guzzleClient->get($url); - } catch (RequestException $e) { - if ($e->getCode() === 404) { - return new ImageDoesNotExistState(); - } - throw $e; - } - - $responseBody = json_decode((string)$response->getBody(), true); - - if ($responseBody['State']['Restarting'] === true) { - return new RestartingState(); - } else { - return new NotRestartingState(); - } - } - - public function GetContainerUpdateState(Container $container) : IContainerState - { - $tag = $container->GetImageTag(); - if ($tag === '%AIO_CHANNEL%') { - $tag = $this->GetCurrentChannel(); - } - - $runningDigests = $this->GetRepoDigestsOfContainer($container->GetIdentifier()); - if ($runningDigests === null) { - return new VersionDifferentState(); - } - $remoteDigest = $this->dockerHubManager->GetLatestDigestOfTag($container->GetContainerName(), $tag); - if ($remoteDigest === null) { - return new VersionEqualstate(); - } - - foreach($runningDigests as $runningDigest) { - if ($runningDigest === $remoteDigest) { - return new VersionEqualState(); - } - } - return new VersionDifferentState(); - } - - public function GetContainerStartingState(Container $container) : IContainerState - { - $runningState = $this->GetContainerRunningState($container); - if ($runningState instanceof StoppedState) { - return new StoppedState(); - } elseif ($runningState instanceof ImageDoesNotExistState) { - return new ImageDoesNotExistState(); - } - - $containerName = $container->GetIdentifier(); - $internalPort = $container->GetInternalPort(); - if($internalPort === '%APACHE_PORT%') { - $internalPort = $this->configurationManager->GetApachePort(); - } elseif($internalPort === '%TALK_PORT%') { - $internalPort = $this->configurationManager->GetTalkPort(); - } - - if ($internalPort !== "" && $internalPort !== 'host') { - $connection = @fsockopen($containerName, (int)$internalPort, $errno, $errstr, 0.2); - if ($connection) { - fclose($connection); - return new RunningState(); - } else { - return new StartingState(); - } - } else { - return new RunningState(); - } - } - - public function DeleteContainer(Container $container) : void { - $url = $this->BuildApiUrl(sprintf('containers/%s?v=true', urlencode($container->GetIdentifier()))); - try { - $this->guzzleClient->delete($url); - } catch (RequestException $e) { - if ($e->getCode() !== 404) { - throw $e; - } - } - } - - public function GetLogs(string $id) : string - { - $url = $this->BuildApiUrl( - sprintf( - 'containers/%s/logs?stdout=true&stderr=true×tamps=true', - urlencode($id) - )); - $responseBody = (string)$this->guzzleClient->get($url)->getBody(); - - $response = ""; - $separator = "\r\n"; - $line = strtok($responseBody, $separator); - $response = substr($line, 8) . $separator; - - while ($line !== false) { - $line = strtok($separator); - $response .= substr($line, 8) . $separator; - } - - return $response; - } - - public function StartContainer(Container $container) : void { - $url = $this->BuildApiUrl(sprintf('containers/%s/start', urlencode($container->GetIdentifier()))); - try { - $this->guzzleClient->post($url); - } catch (RequestException $e) { - throw new \Exception("Could not start container " . $container->GetIdentifier() . ": " . $e->getMessage()); - } - } - - public function CreateVolumes(Container $container): void - { - $url = $this->BuildApiUrl('volumes/create'); - foreach($container->GetVolumes()->GetVolumes() as $volume) { - $forbiddenChars = [ - '/', - ]; - - if ($volume->name === 'nextcloud_aio_nextcloud_datadir' || $volume->name === 'nextcloud_aio_backupdir') { - return; - } - - $firstChar = substr($volume->name, 0, 1); - if(!in_array($firstChar, $forbiddenChars)) { - $this->guzzleClient->request( - 'POST', - $url, - [ - 'json' => [ - 'name' => $volume->name, - ], - ] - ); - } - } - } - - public function CreateContainer(Container $container) : void { - $volumes = []; - foreach ($container->GetVolumes()->GetVolumes() as $volume) { - // // NEXTCLOUD_MOUNT gets added via bind-mount later on - // if ($container->GetIdentifier() === 'nextcloud-aio-nextcloud') { - // if ($volume->name === $this->configurationManager->GetNextcloudMount()) { - // continue; - // } - // } - - $volumeEntry = $volume->name . ':' . $volume->mountPoint; - if ($volume->isWritable) { - $volumeEntry = $volumeEntry . ':' . 'rw'; - } else { - $volumeEntry = $volumeEntry . ':' . 'ro'; - } - - $volumes[] = $volumeEntry; - } - - $requestBody = [ - 'Image' => $this->BuildImageName($container), - ]; - - if (count($volumes) > 0) { - $requestBody['HostConfig']['Binds'] = $volumes; - } - - foreach($container->GetSecrets() as $secret) { - $this->configurationManager->GetAndGenerateSecret($secret); - } - - $aioVariables = $container->GetAioVariables()->GetVariables(); - foreach($aioVariables as $variable) { - $config = $this->configurationManager->GetConfig(); - $variableArray = explode('=', $variable); - $config[$variableArray[0]] = $variableArray[1]; - $this->configurationManager->WriteConfig($config); - sleep(1); - } - - $envs = $container->GetEnvironmentVariables()->GetVariables(); - // Special thing for the nextcloud container - if ($container->GetIdentifier() === 'nextcloud-aio-nextcloud') { - $envs[] = $this->GetAllNextcloudExecCommands(); - } - foreach($envs as $key => $env) { - // TODO: This whole block below is a hack and needs to get reworked in order to support multiple substitutions per line by default for all envs - if (str_starts_with($env, 'extra_params=')) { - $env = str_replace('%COLLABORA_SECCOMP_POLICY%', $this->configurationManager->GetCollaboraSeccompPolicy(), $env); - $env = str_replace('%NC_DOMAIN%', $this->configurationManager->GetDomain(), $env); - $envs[$key] = $env; - continue; - } - - // Original implementation - $patterns = ['/%(.*)%/']; - - if(preg_match($patterns[0], $env, $out) === 1) { - $replacements = array(); - - if($out[1] === 'NC_DOMAIN') { - $replacements[1] = $this->configurationManager->GetDomain(); - } elseif($out[1] === 'NC_BASE_DN') { - $replacements[1] = $this->configurationManager->GetBaseDN(); - } elseif ($out[1] === 'AIO_TOKEN') { - $replacements[1] = $this->configurationManager->GetToken(); - } elseif ($out[1] === 'BORGBACKUP_MODE') { - $replacements[1] = $this->configurationManager->GetBackupMode(); - } elseif ($out[1] === 'AIO_URL') { - $replacements[1] = $this->configurationManager->GetAIOURL(); - } elseif ($out[1] === 'SELECTED_RESTORE_TIME') { - $replacements[1] = $this->configurationManager->GetSelectedRestoreTime(); - } elseif ($out[1] === 'APACHE_PORT') { - $replacements[1] = $this->configurationManager->GetApachePort(); - } elseif ($out[1] === 'TALK_PORT') { - $replacements[1] = $this->configurationManager->GetTalkPort(); - } elseif ($out[1] === 'NEXTCLOUD_MOUNT') { - $replacements[1] = $this->configurationManager->GetNextcloudMount(); - } elseif ($out[1] === 'BACKUP_RESTORE_PASSWORD') { - $replacements[1] = $this->configurationManager->GetBorgRestorePassword(); - } elseif ($out[1] === 'CLAMAV_ENABLED') { - if ($this->configurationManager->isClamavEnabled()) { - $replacements[1] = 'yes'; - } else { - $replacements[1] = ''; - } - } elseif ($out[1] === 'TALK_RECORDING_ENABLED') { - if ($this->configurationManager->isTalkRecordingEnabled()) { - $replacements[1] = 'yes'; - } else { - $replacements[1] = ''; - } - } elseif ($out[1] === 'ONLYOFFICE_ENABLED') { - if ($this->configurationManager->isOnlyofficeEnabled()) { - $replacements[1] = 'yes'; - } else { - $replacements[1] = ''; - } - } elseif ($out[1] === 'COLLABORA_ENABLED') { - if ($this->configurationManager->isCollaboraEnabled()) { - $replacements[1] = 'yes'; - } else { - $replacements[1] = ''; - } - } elseif ($out[1] === 'TALK_ENABLED') { - if ($this->configurationManager->isTalkEnabled()) { - $replacements[1] = 'yes'; - } else { - $replacements[1] = ''; - } - } elseif ($out[1] === 'UPDATE_NEXTCLOUD_APPS') { - if ($this->configurationManager->isDailyBackupRunning() && $this->configurationManager->areAutomaticUpdatesEnabled()) { - $replacements[1] = 'yes'; - } else { - $replacements[1] = ''; - } - } elseif ($out[1] === 'TIMEZONE') { - if ($this->configurationManager->GetTimezone() === '') { - $replacements[1] = 'Etc/UTC'; - } else { - $replacements[1] = $this->configurationManager->GetTimezone(); - } - } elseif ($out[1] === 'COLLABORA_DICTIONARIES') { - if ($this->configurationManager->GetCollaboraDictionaries() === '') { - $replacements[1] = 'de_DE en_GB en_US es_ES fr_FR it nl pt_BR pt_PT ru'; - } else { - $replacements[1] = $this->configurationManager->GetCollaboraDictionaries(); - } - } elseif ($out[1] === 'IMAGINARY_ENABLED') { - if ($this->configurationManager->isImaginaryEnabled()) { - $replacements[1] = 'yes'; - } else { - $replacements[1] = ''; - } - } elseif ($out[1] === 'FULLTEXTSEARCH_ENABLED') { - if ($this->configurationManager->isFulltextsearchEnabled()) { - $replacements[1] = 'yes'; - } else { - $replacements[1] = ''; - } - } elseif ($out[1] === 'DOCKER_SOCKET_PROXY_ENABLED') { - if ($this->configurationManager->isDockerSocketProxyEnabled()) { - $replacements[1] = 'yes'; - } else { - $replacements[1] = ''; - } - } elseif ($out[1] === 'NEXTCLOUD_UPLOAD_LIMIT') { - $replacements[1] = $this->configurationManager->GetNextcloudUploadLimit(); - } elseif ($out[1] === 'NEXTCLOUD_MEMORY_LIMIT') { - $replacements[1] = $this->configurationManager->GetNextcloudMemoryLimit(); - } elseif ($out[1] === 'NEXTCLOUD_MAX_TIME') { - $replacements[1] = $this->configurationManager->GetNextcloudMaxTime(); - } elseif ($out[1] === 'BORG_RETENTION_POLICY') { - $replacements[1] = $this->configurationManager->GetBorgRetentionPolicy(); - } elseif ($out[1] === 'NEXTCLOUD_TRUSTED_CACERTS_DIR') { - $replacements[1] = $this->configurationManager->GetTrustedCacertsDir(); - } elseif ($out[1] === 'ADDITIONAL_DIRECTORIES_BACKUP') { - if ($this->configurationManager->GetAdditionalBackupDirectoriesString() !== '') { - $replacements[1] = 'yes'; - } else { - $replacements[1] = ''; - } - } elseif ($out[1] === 'BORGBACKUP_HOST_LOCATION') { - $replacements[1] = $this->configurationManager->GetBorgBackupHostLocation(); - } elseif ($out[1] === 'APACHE_MAX_SIZE') { - $replacements[1] = $this->configurationManager->GetApacheMaxSize(); - } elseif ($out[1] === 'COLLABORA_SECCOMP_POLICY') { - $replacements[1] = $this->configurationManager->GetCollaboraSeccompPolicy(); - } elseif ($out[1] === 'NEXTCLOUD_STARTUP_APPS') { - $replacements[1] = $this->configurationManager->GetNextcloudStartupApps(); - } elseif ($out[1] === 'NEXTCLOUD_ADDITIONAL_APKS') { - $replacements[1] = $this->configurationManager->GetNextcloudAdditionalApks(); - } elseif ($out[1] === 'NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS') { - $replacements[1] = $this->configurationManager->GetNextcloudAdditionalPhpExtensions(); - } elseif ($out[1] === 'INSTALL_LATEST_MAJOR') { - if ($this->configurationManager->shouldLatestMajorGetInstalled()) { - $replacements[1] = 'yes'; - } else { - $replacements[1] = ''; - } - } elseif ($out[1] === 'REMOVE_DISABLED_APPS') { - if ($this->configurationManager->shouldDisabledAppsGetRemoved()) { - $replacements[1] = 'yes'; - } else { - $replacements[1] = ''; - } - // Allow to get local ip-address of database container which allows to talk to it even in host mode (the container that requires this needs to be started first then) - } elseif ($out[1] === 'AIO_DATABASE_HOST') { - $replacements[1] = gethostbyname('nextcloud-aio-database'); - // Allow to get local ip-address of caddy container and add it to trusted proxies automatically - } elseif ($out[1] === 'CADDY_IP_ADDRESS') { - $replacements[1] = ''; - $communityContainers = $this->configurationManager->GetEnabledCommunityContainers(); - if (in_array('caddy', $communityContainers, true)) { - $replacements[1] = gethostbyname('nextcloud-aio-caddy'); - } - } elseif ($out[1] === 'WHITEBOARD_ENABLED') { - if ($this->configurationManager->isWhiteboardEnabled()) { - $replacements[1] = 'yes'; - } else { - $replacements[1] = ''; - } - } else { - $secret = $this->configurationManager->GetSecret($out[1]); - if ($secret === "") { - throw new \Exception("The secret " . $out[1] . " is empty. Cannot substitute its value. Please check if it is defined in secrets of containers.json."); - } - $replacements[1] = $secret; - } - - $envs[$key] = preg_replace($patterns, $replacements, $env); - } - } - - if(count($envs) > 0) { - $requestBody['Env'] = $envs; - } - - $requestBody['HostConfig']['RestartPolicy']['Name'] = $container->GetRestartPolicy(); - - $requestBody['HostConfig']['ReadonlyRootfs'] = $container->GetReadOnlySetting(); - - $exposedPorts = []; - if ($container->GetInternalPort() !== 'host') { - foreach($container->GetPorts()->GetPorts() as $value) { - $port = $value->port; - $protocol = $value->protocol; - if ($port === '%APACHE_PORT%') { - $port = $this->configurationManager->GetApachePort(); - // Do not expose udp if AIO is in reverse proxy mode - if ($port !== '443' && $protocol === 'udp') { - continue; - } - } else if ($port === '%TALK_PORT%') { - $port = $this->configurationManager->GetTalkPort(); - } - $portWithProtocol = $port . '/' . $protocol; - $exposedPorts[$portWithProtocol] = null; - } - $requestBody['HostConfig']['NetworkMode'] = 'nextcloud-aio'; - } else { - $requestBody['HostConfig']['NetworkMode'] = 'host'; - } - - if(count($exposedPorts) > 0) { - $requestBody['ExposedPorts'] = $exposedPorts; - foreach ($container->GetPorts()->GetPorts() as $value) { - $port = $value->port; - $protocol = $value->protocol; - if ($port === '%APACHE_PORT%') { - $port = $this->configurationManager->GetApachePort(); - // Do not expose udp if AIO is in reverse proxy mode - if ($port !== '443' && $protocol === 'udp') { - continue; - } - } else if ($port === '%TALK_PORT%') { - $port = $this->configurationManager->GetTalkPort(); - } - $ipBinding = $value->ipBinding; - if ($ipBinding === '%APACHE_IP_BINDING%') { - $ipBinding = $this->configurationManager->GetApacheIPBinding(); - // Do not expose if AIO is in internal network mode - if ($ipBinding === '@INTERNAL') { - continue; - } - } - $portWithProtocol = $port . '/' . $protocol; - $requestBody['HostConfig']['PortBindings'][$portWithProtocol] = [ - [ - 'HostPort' => $port, - 'HostIp' => $ipBinding, - ] - ]; - } - } - - $devices = []; - foreach($container->GetDevices() as $device) { - if ($device === '/dev/dri' && ! $this->configurationManager->isDriDeviceEnabled()) { - continue; - } - $devices[] = ["PathOnHost" => $device, "PathInContainer" => $device, "CgroupPermissions" => "rwm"]; - } - - if (count($devices) > 0) { - $requestBody['HostConfig']['Devices'] = $devices; - } - - $shmSize = $container->GetShmSize(); - if ($shmSize > 0) { - $requestBody['HostConfig']['ShmSize'] = $shmSize; - } - - $tmpfs = []; - foreach($container->GetTmpfs() as $tmp) { - $mode = ""; - if (str_contains($tmp, ':')) { - $mode = explode(':', $tmp)[1]; - $tmp = explode(':', $tmp)[0]; - } - $tmpfs[$tmp] = $mode; - } - if (count($tmpfs) > 0) { - $requestBody['HostConfig']['Tmpfs'] = $tmpfs; - } - - $requestBody['HostConfig']['Init'] = $container->GetInit(); - - $capAdds = $container->GetCapAdds(); - if (count($capAdds) > 0) { - $requestBody['HostConfig']['CapAdd'] = $capAdds; - } - - // Disable arp spoofing - if (!in_array('NET_RAW', $capAdds, true)) { - $requestBody['HostConfig']['CapDrop'] = ['NET_RAW']; - } - - // Disable SELinux for AIO containers so that it does not break them - $requestBody['HostConfig']['SecurityOpt'] = ["label:disable"]; - if ($container->isApparmorUnconfined()) { - $requestBody['HostConfig']['SecurityOpt'] = ["apparmor:unconfined", "label:disable"]; - } - - $mounts = []; - - // Special things for the backup container which should not be exposed in the containers.json - if ($container->GetIdentifier() === 'nextcloud-aio-borgbackup') { - // Additional backup directories - foreach ($this->getAllBackupVolumes() as $additionalBackupVolumes) { - if ($additionalBackupVolumes !== '') { - $mounts[] = ["Type" => "volume", "Source" => $additionalBackupVolumes, "Target" => "/nextcloud_aio_volumes/" . $additionalBackupVolumes, "ReadOnly" => false]; - } - } - foreach ($this->configurationManager->GetAdditionalBackupDirectoriesArray() as $additionalBackupDirectories) { - if ($additionalBackupDirectories !== '') { - if (!str_starts_with($additionalBackupDirectories, '/')) { - $mounts[] = ["Type" => "volume", "Source" => $additionalBackupDirectories, "Target" => "/docker_volumes/" . $additionalBackupDirectories, "ReadOnly" => true]; - } else { - $mounts[] = ["Type" => "bind", "Source" => $additionalBackupDirectories, "Target" => "/host_mounts" . $additionalBackupDirectories, "ReadOnly" => true, "BindOptions" => ["NonRecursive" => true]]; - } - } - } - // Special things for the talk container which should not be exposed in the containers.json - } elseif ($container->GetIdentifier() === 'nextcloud-aio-talk') { - // This is needed due to a bug in libwebsockets which cannot handle unlimited ulimits - $requestBody['HostConfig']['Ulimits'] = [["Name" => "nofile", "Hard" => 200000, "Soft" => 200000]]; - // // Special things for the nextcloud container which should not be exposed in the containers.json - // } elseif ($container->GetIdentifier() === 'nextcloud-aio-nextcloud') { - // foreach ($container->GetVolumes()->GetVolumes() as $volume) { - // if ($volume->name !== $this->configurationManager->GetNextcloudMount()) { - // continue; - // } - // $mounts[] = ["Type" => "bind", "Source" => $volume->name, "Target" => $volume->mountPoint, "ReadOnly" => !$volume->isWritable, "BindOptions" => [ "Propagation" => "rshared"]]; - // } - // Special things for the caddy community container - } elseif ($container->GetIdentifier() === 'nextcloud-aio-caddy') { - $requestBody['HostConfig']['ExtraHosts'] = ['host.docker.internal:host-gateway']; - } - - if (count($mounts) > 0) { - $requestBody['HostConfig']['Mounts'] = $mounts; - } - - $url = $this->BuildApiUrl('containers/create?name=' . $container->GetIdentifier()); - try { - $this->guzzleClient->request( - 'POST', - $url, - [ - 'json' => $requestBody - ] - ); - } catch (RequestException $e) { - throw new \Exception("Could not create container " . $container->GetIdentifier() . ": " . $e->getMessage()); - } - - } - - public function isDockerHubReachable(Container $container) : bool { - $tag = $container->GetImageTag(); - if ($tag === '%AIO_CHANNEL%') { - $tag = $this->GetCurrentChannel(); - } - - $remoteDigest = $this->dockerHubManager->GetLatestDigestOfTag($container->GetContainerName(), $tag); - - if ($remoteDigest === null) { - return false; - } else { - return true; - } - } - - public function PullImage(Container $container) : void - { - $imageName = $this->BuildImageName($container); - $encodedImageName = urlencode($imageName); - $url = $this->BuildApiUrl(sprintf('images/create?fromImage=%s', $encodedImageName)); - $imageIsThere = true; - try { - $imageUrl = $this->BuildApiUrl(sprintf('images/%s/json', $encodedImageName)); - $this->guzzleClient->get($imageUrl)->getBody()->getContents(); - } catch (\Throwable $e) { - $imageIsThere = false; - } - try { - $this->guzzleClient->post($url); - } catch (RequestException $e) { - if ($imageIsThere === false) { - throw new \Exception("Could not pull image " . $imageName . ". Please run 'sudo docker exec -it nextcloud-aio-mastercontainer docker pull " . $imageName . "' in order to find out why it failed."); - } - } - } - - private function isContainerUpdateAvailable(string $id) : string - { - $container = $this->containerDefinitionFetcher->GetContainerById($id); - - $updateAvailable = ""; - if ($container->GetUpdateState() instanceof VersionDifferentState) { - $updateAvailable = '1'; - } - foreach ($container->GetDependsOn() as $dependency) { - $updateAvailable .= $this->isContainerUpdateAvailable($dependency); - } - return $updateAvailable; - } - - public function isAnyUpdateAvailable() : bool { - // return early if instance is not installed - if (!$this->configurationManager->wasStartButtonClicked()) { - return false; - } - $id = 'nextcloud-aio-apache'; - - if ($this->isContainerUpdateAvailable($id) !== "") { - return true; - } else { - return false; - } - } - - private function getBackupVolumes(string $id) : string - { - $container = $this->containerDefinitionFetcher->GetContainerById($id); - - $backupVolumes = ''; - foreach ($container->GetBackupVolumes() as $backupVolume) { - $backupVolumes .= $backupVolume . ' '; - } - foreach ($container->GetDependsOn() as $dependency) { - $backupVolumes .= $this->getBackupVolumes($dependency); - } - return $backupVolumes; - } - - private function getAllBackupVolumes() : array { - $id = 'nextcloud-aio-apache'; - $backupVolumesArray = explode(' ', $this->getBackupVolumes($id)); - return array_unique($backupVolumesArray); - } - - private function GetNextcloudExecCommands(string $id) : string - { - $container = $this->containerDefinitionFetcher->GetContainerById($id); - - $nextcloudExecCommands = ''; - foreach ($container->GetNextcloudExecCommands() as $execCommand) { - $nextcloudExecCommands .= $execCommand . PHP_EOL; - } - foreach ($container->GetDependsOn() as $dependency) { - $nextcloudExecCommands .= $this->GetNextcloudExecCommands($dependency); - } - return $nextcloudExecCommands; - } - - private function GetAllNextcloudExecCommands() : string - { - $id = 'nextcloud-aio-apache'; - return 'NEXTCLOUD_EXEC_COMMANDS=' . $this->GetNextcloudExecCommands($id); - } - - private function GetRepoDigestsOfContainer(string $containerName) : ?array { - try { - $containerUrl = $this->BuildApiUrl(sprintf('containers/%s/json', $containerName)); - $containerOutput = json_decode($this->guzzleClient->get($containerUrl)->getBody()->getContents(), true); - $imageName = $containerOutput['Image']; - - $imageUrl = $this->BuildApiUrl(sprintf('images/%s/json', $imageName)); - $imageOutput = json_decode($this->guzzleClient->get($imageUrl)->getBody()->getContents(), true); - - if (!isset($imageOutput['RepoDigests'])) { - error_log('RepoDigests is not set of container ' . $containerName); - return null; - } - - if (!is_array($imageOutput['RepoDigests'])) { - error_log('RepoDigests of ' . $containerName . ' is not an array which is not allowed!'); - return null; - } - - $repoDigestArray = []; - $oneDigestGiven = false; - foreach($imageOutput['RepoDigests'] as $repoDigest) { - $digestPosition = strpos($repoDigest, '@'); - if ($digestPosition === false) { - error_log('Somehow the RepoDigest of ' . $containerName . ' does not contain a @.'); - return null; - } - $repoDigestArray[] = substr($repoDigest, $digestPosition + 1); - $oneDigestGiven = true; - } - - if ($oneDigestGiven) { - return $repoDigestArray; - } - - return null; - } catch (\Exception $e) { - return null; - } - } - - public function GetCurrentChannel() : string { - $cacheKey = 'aio-ChannelName'; - $channelName = apcu_fetch($cacheKey); - if($channelName !== false && is_string($channelName)) { - return $channelName; - } - - $containerName = 'nextcloud-aio-mastercontainer'; - $url = $this->BuildApiUrl(sprintf('containers/%s/json', $containerName)); - try { - $output = json_decode($this->guzzleClient->get($url)->getBody()->getContents(), true); - $containerChecksum = $output['Image']; - $tagArray = explode(':', $output['Config']['Image']); - $tag = $tagArray[1]; - apcu_add($cacheKey, $tag); - /** - * @psalm-suppress TypeDoesNotContainNull - * @psalm-suppress DocblockTypeContradiction - */ - if ($tag === null) { - error_log("No tag was found when getting the current channel. You probably did not follow the documentation correctly. Changing the channel to the default 'latest'."); - $tag = 'latest'; - } - return $tag; - } catch (\Exception $e) { - error_log('Could not get current channel ' . $e->getMessage()); - } - - return 'latest'; - } - - public function IsMastercontainerUpdateAvailable() : bool - { - $imageName = 'nextcloud/all-in-one'; - $containerName = 'nextcloud-aio-mastercontainer'; - - $tag = $this->GetCurrentChannel(); - - $runningDigests = $this->GetRepoDigestsOfContainer($containerName); - if ($runningDigests === null) { - return true; - } - $remoteDigest = $this->dockerHubManager->GetLatestDigestOfTag($imageName, $tag); - if ($remoteDigest === null) { - return false; - } - - foreach ($runningDigests as $runningDigest) { - if ($remoteDigest === $runningDigest) { - return false; - } - } - return true; - } - - public function sendNotification(Container $container, string $subject, string $message, string $file = '/notify.sh') : void - { - if ($this->GetContainerStartingState($container) instanceof RunningState) { - - $containerName = $container->GetIdentifier(); - - // schedule the exec - $url = $this->BuildApiUrl(sprintf('containers/%s/exec', urlencode($containerName))); - $response = json_decode( - $this->guzzleClient->request( - 'POST', - $url, - [ - 'json' => [ - 'AttachStdout' => true, - 'Tty' => true, - 'Cmd' => [ - 'bash', - $file, - $subject, - $message - ], - ], - ] - )->getBody()->getContents(), - true - ); - - $id = $response['Id']; - - // start the exec - $url = $this->BuildApiUrl(sprintf('exec/%s/start', $id)); - $this->guzzleClient->request( - 'POST', - $url, - [ - 'json' => [ - 'Detach' => false, - 'Tty' => true, - ], - ] - ); - } - } - - private function DisconnectContainerFromBridgeNetwork(string $id) : void - { - - $url = $this->BuildApiUrl( - sprintf('networks/%s/disconnect', 'bridge') - ); - - try { - $this->guzzleClient->request( - 'POST', - $url, - [ - 'json' => [ - 'container' => $id, - ], - ] - ); - } catch (RequestException $e) { - } - } - - private function ConnectContainerIdToNetwork(string $id, string $internalPort, string $network = 'nextcloud-aio') : void - { - if ($internalPort === 'host') { - return; - } - - $url = $this->BuildApiUrl('networks/create'); - try { - $this->guzzleClient->request( - 'POST', - $url, - [ - 'json' => [ - 'Name' => $network, - 'CheckDuplicate' => true, - 'Driver' => 'bridge', - 'Internal' => false, - ] - ] - ); - } catch (RequestException $e) { - // 409 is undocumented and gets thrown if the network already exists. - if ($e->getCode() !== 409) { - throw new \Exception("Could not create the nextcloud-aio network: " . $e->getMessage()); - } - } - - $url = $this->BuildApiUrl( - sprintf('networks/%s/connect', $network) - ); - try { - $this->guzzleClient->request( - 'POST', - $url, - [ - 'json' => [ - 'container' => $id, - ] - ] - ); - } catch (RequestException $e) { - // 403 is undocumented and gets thrown if a specific container is already part of a network - if ($e->getCode() !== 403) { - throw $e; - } - } - } - - public function ConnectMasterContainerToNetwork() : void - { - $this->ConnectContainerIdToNetwork('nextcloud-aio-mastercontainer', ''); - // Don't disconnect here since it slows down the initial login by a lot. Is getting done during cron.sh instead. - // $this->DisconnectContainerFromBridgeNetwork('nextcloud-aio-mastercontainer'); - } - - public function ConnectContainerToNetwork(Container $container) : void - { - $this->ConnectContainerIdToNetwork($container->GetIdentifier(), $container->GetInternalPort()); - } - - public function StopContainer(Container $container) : void { - $url = $this->BuildApiUrl(sprintf('containers/%s/stop?t=%s', urlencode($container->GetIdentifier()), $container->GetMaxShutdownTime())); - try { - $this->guzzleClient->post($url); - } catch (RequestException $e) { - if ($e->getCode() !== 404 && $e->getCode() !== 304) { - throw $e; - } - } - } - - public function GetBackupcontainerExitCode() : int - { - $containerName = 'nextcloud-aio-borgbackup'; - $url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($containerName))); - try { - $response = $this->guzzleClient->get($url); - } catch (RequestException $e) { - if ($e->getCode() === 404) { - return -1; - } - throw $e; - } - - $responseBody = json_decode((string)$response->getBody(), true); - - $exitCode = $responseBody['State']['ExitCode']; - if (is_int($exitCode)) { - return $exitCode; - } else { - return -1; - } - } - - public function GetDatabasecontainerExitCode() : int - { - $containerName = 'nextcloud-aio-database'; - $url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($containerName))); - try { - $response = $this->guzzleClient->get($url); - } catch (RequestException $e) { - if ($e->getCode() === 404) { - return -1; - } - throw $e; - } - - $responseBody = json_decode((string)$response->getBody(), true); - - $exitCode = $responseBody['State']['ExitCode']; - if (is_int($exitCode)) { - return $exitCode; - } else { - return -1; - } - } - - public function isLoginAllowed() : bool { - $id = 'nextcloud-aio-apache'; - $apacheContainer = $this->containerDefinitionFetcher->GetContainerById($id); - if ($this->GetContainerStartingState($apacheContainer) instanceof RunningState) { - return false; - } - return true; - } - - public function isBackupContainerRunning() : bool { - $id = 'nextcloud-aio-borgbackup'; - $backupContainer = $this->containerDefinitionFetcher->GetContainerById($id); - if ($this->GetContainerRunningState($backupContainer) instanceof RunningState) { - return true; - } - return false; - } - - private function GetCreatedTimeOfNextcloudImage() : ?string { - $imageName = 'nextcloud/aio-nextcloud' . ':' . $this->GetCurrentChannel(); - try { - $imageUrl = $this->BuildApiUrl(sprintf('images/%s/json', $imageName)); - $imageOutput = json_decode($this->guzzleClient->get($imageUrl)->getBody()->getContents(), true); - - if (!isset($imageOutput['Created'])) { - error_log('Created is not set of image ' . $imageName); - return null; - } - - return str_replace('T', ' ', (string)$imageOutput['Created']); - } catch (\Exception $e) { - return null; - } - } - - public function isNextcloudImageOutdated() : bool { - $createdTime = $this->GetCreatedTimeOfNextcloudImage(); - - if ($createdTime === null) { - return false; - } - - // If the image is older than 90 days, it is outdated. - if ((time() - (60 * 60 * 24 * 90)) > strtotime($createdTime)) { - return true; - } - - return false; - } + +class DockerActionManager { + private const string API_VERSION = 'v1.41'; + private \GuzzleHttp\Client $guzzleClient; + private ConfigurationManager $configurationManager; + private ContainerDefinitionFetcher $containerDefinitionFetcher; + private DockerHubManager $dockerHubManager; + + public function __construct( + ConfigurationManager $configurationManager, + ContainerDefinitionFetcher $containerDefinitionFetcher, + DockerHubManager $dockerHubManager, + ) { + $this->configurationManager = $configurationManager; + $this->containerDefinitionFetcher = $containerDefinitionFetcher; + $this->dockerHubManager = $dockerHubManager; + $this->guzzleClient = new \GuzzleHttp\Client( + [ + 'curl' => [ + CURLOPT_UNIX_SOCKET_PATH => '/var/run/docker.sock', + + ], + ] + ); + } + + private function BuildApiUrl(string $url) : string { + return sprintf('http://127.0.0.1/%s/%s', self::API_VERSION, $url); + } + + private function BuildImageName(Container $container) : string { + $tag = $container->GetImageTag(); + if ($tag === '%AIO_CHANNEL%') { + $tag = $this->GetCurrentChannel(); + } + return $container->GetContainerName() . ':' . $tag; + } + + public function GetContainerRunningState(Container $container) : IContainerState { + $url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($container->GetIdentifier()))); + try { + $response = $this->guzzleClient->get($url); + } catch (RequestException $e) { + if ($e->getCode() === 404) { + return new ImageDoesNotExistState(); + } + throw $e; + } + + $responseBody = json_decode((string)$response->getBody(), true); + + if ($responseBody['State']['Running'] === true) { + return new RunningState(); + } else { + return new StoppedState(); + } + } + + public function GetContainerRestartingState(Container $container) : IContainerState { + $url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($container->GetIdentifier()))); + try { + $response = $this->guzzleClient->get($url); + } catch (RequestException $e) { + if ($e->getCode() === 404) { + return new ImageDoesNotExistState(); + } + throw $e; + } + + $responseBody = json_decode((string)$response->getBody(), true); + + if ($responseBody['State']['Restarting'] === true) { + return new RestartingState(); + } else { + return new NotRestartingState(); + } + } + + public function GetContainerUpdateState(Container $container) : IContainerState { + $tag = $container->GetImageTag(); + if ($tag === '%AIO_CHANNEL%') { + $tag = $this->GetCurrentChannel(); + } + + $runningDigests = $this->GetRepoDigestsOfContainer($container->GetIdentifier()); + if ($runningDigests === null) { + return new VersionDifferentState(); + } + $remoteDigest = $this->dockerHubManager->GetLatestDigestOfTag($container->GetContainerName(), $tag); + if ($remoteDigest === null) { + return new VersionEqualstate(); + } + + foreach ($runningDigests as $runningDigest) { + if ($runningDigest === $remoteDigest) { + return new VersionEqualState(); + } + } + return new VersionDifferentState(); + } + + public function GetContainerStartingState(Container $container) : IContainerState { + $runningState = $this->GetContainerRunningState($container); + if ($runningState instanceof StoppedState) { + return new StoppedState(); + } elseif ($runningState instanceof ImageDoesNotExistState) { + return new ImageDoesNotExistState(); + } + + $containerName = $container->GetIdentifier(); + $internalPort = $container->GetInternalPort(); + if ($internalPort === '%APACHE_PORT%') { + $internalPort = $this->configurationManager->GetApachePort(); + } elseif ($internalPort === '%TALK_PORT%') { + $internalPort = $this->configurationManager->GetTalkPort(); + } + + if ($internalPort !== '' && $internalPort !== 'host') { + $connection = @fsockopen($containerName, (int)$internalPort, $errno, $errstr, 0.2); + if ($connection) { + fclose($connection); + return new RunningState(); + } else { + return new StartingState(); + } + } else { + return new RunningState(); + } + } + + public function DeleteContainer(Container $container) : void { + $url = $this->BuildApiUrl(sprintf('containers/%s?v=true', urlencode($container->GetIdentifier()))); + try { + $this->guzzleClient->delete($url); + } catch (RequestException $e) { + if ($e->getCode() !== 404) { + throw $e; + } + } + } + + public function GetLogs(string $id) : string { + $url = $this->BuildApiUrl( + sprintf( + 'containers/%s/logs?stdout=true&stderr=true×tamps=true', + urlencode($id) + )); + $responseBody = (string)$this->guzzleClient->get($url)->getBody(); + + $response = ''; + $separator = "\r\n"; + $line = strtok($responseBody, $separator); + $response = substr($line, 8) . $separator; + + while ($line !== false) { + $line = strtok($separator); + $response .= substr($line, 8) . $separator; + } + + return $response; + } + + public function StartContainer(Container $container) : void { + $url = $this->BuildApiUrl(sprintf('containers/%s/start', urlencode($container->GetIdentifier()))); + try { + $this->guzzleClient->post($url); + } catch (RequestException $e) { + throw new \Exception('Could not start container ' . $container->GetIdentifier() . ': ' . $e->getMessage()); + } + } + + public function CreateVolumes(Container $container): void { + $url = $this->BuildApiUrl('volumes/create'); + foreach ($container->GetVolumes()->GetVolumes() as $volume) { + $forbiddenChars = [ + '/', + ]; + + if ($volume->name === 'nextcloud_aio_nextcloud_datadir' || $volume->name === 'nextcloud_aio_backupdir') { + return; + } + + $firstChar = substr($volume->name, 0, 1); + if (!in_array($firstChar, $forbiddenChars)) { + $this->guzzleClient->request( + 'POST', + $url, + [ + 'json' => [ + 'name' => $volume->name, + ], + ] + ); + } + } + } + + public function CreateContainer(Container $container) : void { + $volumes = []; + foreach ($container->GetVolumes()->GetVolumes() as $volume) { + // // NEXTCLOUD_MOUNT gets added via bind-mount later on + // if ($container->GetIdentifier() === 'nextcloud-aio-nextcloud') { + // if ($volume->name === $this->configurationManager->GetNextcloudMount()) { + // continue; + // } + // } + + $volumeEntry = $volume->name . ':' . $volume->mountPoint; + if ($volume->isWritable) { + $volumeEntry = $volumeEntry . ':' . 'rw'; + } else { + $volumeEntry = $volumeEntry . ':' . 'ro'; + } + + $volumes[] = $volumeEntry; + } + + $requestBody = [ + 'Image' => $this->BuildImageName($container), + ]; + + if (count($volumes) > 0) { + $requestBody['HostConfig']['Binds'] = $volumes; + } + + foreach ($container->GetSecrets() as $secret) { + $this->configurationManager->GetAndGenerateSecret($secret); + } + + $aioVariables = $container->GetAioVariables()->GetVariables(); + foreach ($aioVariables as $variable) { + $config = $this->configurationManager->GetConfig(); + $variableArray = explode('=', $variable); + $config[$variableArray[0]] = $variableArray[1]; + $this->configurationManager->WriteConfig($config); + sleep(1); + } + + $envs = $container->GetEnvironmentVariables()->GetVariables(); + // Special thing for the nextcloud container + if ($container->GetIdentifier() === 'nextcloud-aio-nextcloud') { + $envs[] = $this->GetAllNextcloudExecCommands(); + } + foreach ($envs as $key => $env) { + // TODO: This whole block below is a hack and needs to get reworked in order to support multiple substitutions per line by default for all envs + if (str_starts_with($env, 'extra_params=')) { + $env = str_replace('%COLLABORA_SECCOMP_POLICY%', $this->configurationManager->GetCollaboraSeccompPolicy(), $env); + $env = str_replace('%NC_DOMAIN%', $this->configurationManager->GetDomain(), $env); + $envs[$key] = $env; + continue; + } + + // Original implementation + $patterns = ['/%(.*)%/']; + + if (preg_match($patterns[0], $env, $out) === 1) { + $replacements = []; + + if ($out[1] === 'NC_DOMAIN') { + $replacements[1] = $this->configurationManager->GetDomain(); + } elseif ($out[1] === 'NC_BASE_DN') { + $replacements[1] = $this->configurationManager->GetBaseDN(); + } elseif ($out[1] === 'AIO_TOKEN') { + $replacements[1] = $this->configurationManager->GetToken(); + } elseif ($out[1] === 'BORGBACKUP_MODE') { + $replacements[1] = $this->configurationManager->GetBackupMode(); + } elseif ($out[1] === 'AIO_URL') { + $replacements[1] = $this->configurationManager->GetAIOURL(); + } elseif ($out[1] === 'SELECTED_RESTORE_TIME') { + $replacements[1] = $this->configurationManager->GetSelectedRestoreTime(); + } elseif ($out[1] === 'APACHE_PORT') { + $replacements[1] = $this->configurationManager->GetApachePort(); + } elseif ($out[1] === 'TALK_PORT') { + $replacements[1] = $this->configurationManager->GetTalkPort(); + } elseif ($out[1] === 'NEXTCLOUD_MOUNT') { + $replacements[1] = $this->configurationManager->GetNextcloudMount(); + } elseif ($out[1] === 'BACKUP_RESTORE_PASSWORD') { + $replacements[1] = $this->configurationManager->GetBorgRestorePassword(); + } elseif ($out[1] === 'CLAMAV_ENABLED') { + if ($this->configurationManager->isClamavEnabled()) { + $replacements[1] = 'yes'; + } else { + $replacements[1] = ''; + } + } elseif ($out[1] === 'TALK_RECORDING_ENABLED') { + if ($this->configurationManager->isTalkRecordingEnabled()) { + $replacements[1] = 'yes'; + } else { + $replacements[1] = ''; + } + } elseif ($out[1] === 'ONLYOFFICE_ENABLED') { + if ($this->configurationManager->isOnlyofficeEnabled()) { + $replacements[1] = 'yes'; + } else { + $replacements[1] = ''; + } + } elseif ($out[1] === 'COLLABORA_ENABLED') { + if ($this->configurationManager->isCollaboraEnabled()) { + $replacements[1] = 'yes'; + } else { + $replacements[1] = ''; + } + } elseif ($out[1] === 'TALK_ENABLED') { + if ($this->configurationManager->isTalkEnabled()) { + $replacements[1] = 'yes'; + } else { + $replacements[1] = ''; + } + } elseif ($out[1] === 'UPDATE_NEXTCLOUD_APPS') { + if ($this->configurationManager->isDailyBackupRunning() && $this->configurationManager->areAutomaticUpdatesEnabled()) { + $replacements[1] = 'yes'; + } else { + $replacements[1] = ''; + } + } elseif ($out[1] === 'TIMEZONE') { + if ($this->configurationManager->GetTimezone() === '') { + $replacements[1] = 'Etc/UTC'; + } else { + $replacements[1] = $this->configurationManager->GetTimezone(); + } + } elseif ($out[1] === 'COLLABORA_DICTIONARIES') { + if ($this->configurationManager->GetCollaboraDictionaries() === '') { + $replacements[1] = 'de_DE en_GB en_US es_ES fr_FR it nl pt_BR pt_PT ru'; + } else { + $replacements[1] = $this->configurationManager->GetCollaboraDictionaries(); + } + } elseif ($out[1] === 'IMAGINARY_ENABLED') { + if ($this->configurationManager->isImaginaryEnabled()) { + $replacements[1] = 'yes'; + } else { + $replacements[1] = ''; + } + } elseif ($out[1] === 'FULLTEXTSEARCH_ENABLED') { + if ($this->configurationManager->isFulltextsearchEnabled()) { + $replacements[1] = 'yes'; + } else { + $replacements[1] = ''; + } + } elseif ($out[1] === 'DOCKER_SOCKET_PROXY_ENABLED') { + if ($this->configurationManager->isDockerSocketProxyEnabled()) { + $replacements[1] = 'yes'; + } else { + $replacements[1] = ''; + } + } elseif ($out[1] === 'NEXTCLOUD_UPLOAD_LIMIT') { + $replacements[1] = $this->configurationManager->GetNextcloudUploadLimit(); + } elseif ($out[1] === 'NEXTCLOUD_MEMORY_LIMIT') { + $replacements[1] = $this->configurationManager->GetNextcloudMemoryLimit(); + } elseif ($out[1] === 'NEXTCLOUD_MAX_TIME') { + $replacements[1] = $this->configurationManager->GetNextcloudMaxTime(); + } elseif ($out[1] === 'BORG_RETENTION_POLICY') { + $replacements[1] = $this->configurationManager->GetBorgRetentionPolicy(); + } elseif ($out[1] === 'NEXTCLOUD_TRUSTED_CACERTS_DIR') { + $replacements[1] = $this->configurationManager->GetTrustedCacertsDir(); + } elseif ($out[1] === 'ADDITIONAL_DIRECTORIES_BACKUP') { + if ($this->configurationManager->GetAdditionalBackupDirectoriesString() !== '') { + $replacements[1] = 'yes'; + } else { + $replacements[1] = ''; + } + } elseif ($out[1] === 'BORGBACKUP_HOST_LOCATION') { + $replacements[1] = $this->configurationManager->GetBorgBackupHostLocation(); + } elseif ($out[1] === 'APACHE_MAX_SIZE') { + $replacements[1] = $this->configurationManager->GetApacheMaxSize(); + } elseif ($out[1] === 'COLLABORA_SECCOMP_POLICY') { + $replacements[1] = $this->configurationManager->GetCollaboraSeccompPolicy(); + } elseif ($out[1] === 'NEXTCLOUD_STARTUP_APPS') { + $replacements[1] = $this->configurationManager->GetNextcloudStartupApps(); + } elseif ($out[1] === 'NEXTCLOUD_ADDITIONAL_APKS') { + $replacements[1] = $this->configurationManager->GetNextcloudAdditionalApks(); + } elseif ($out[1] === 'NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS') { + $replacements[1] = $this->configurationManager->GetNextcloudAdditionalPhpExtensions(); + } elseif ($out[1] === 'INSTALL_LATEST_MAJOR') { + if ($this->configurationManager->shouldLatestMajorGetInstalled()) { + $replacements[1] = 'yes'; + } else { + $replacements[1] = ''; + } + } elseif ($out[1] === 'REMOVE_DISABLED_APPS') { + if ($this->configurationManager->shouldDisabledAppsGetRemoved()) { + $replacements[1] = 'yes'; + } else { + $replacements[1] = ''; + } + // Allow to get local ip-address of database container which allows to talk to it even in host mode (the container that requires this needs to be started first then) + } elseif ($out[1] === 'AIO_DATABASE_HOST') { + $replacements[1] = gethostbyname('nextcloud-aio-database'); + // Allow to get local ip-address of caddy container and add it to trusted proxies automatically + } elseif ($out[1] === 'CADDY_IP_ADDRESS') { + $replacements[1] = ''; + $communityContainers = $this->configurationManager->GetEnabledCommunityContainers(); + if (in_array('caddy', $communityContainers, true)) { + $replacements[1] = gethostbyname('nextcloud-aio-caddy'); + } + } elseif ($out[1] === 'WHITEBOARD_ENABLED') { + if ($this->configurationManager->isWhiteboardEnabled()) { + $replacements[1] = 'yes'; + } else { + $replacements[1] = ''; + } + } else { + $secret = $this->configurationManager->GetSecret($out[1]); + if ($secret === '') { + throw new \Exception('The secret ' . $out[1] . ' is empty. Cannot substitute its value. Please check if it is defined in secrets of containers.json.'); + } + $replacements[1] = $secret; + } + + $envs[$key] = preg_replace($patterns, $replacements, $env); + } + } + + if (count($envs) > 0) { + $requestBody['Env'] = $envs; + } + + $requestBody['HostConfig']['RestartPolicy']['Name'] = $container->GetRestartPolicy(); + + $requestBody['HostConfig']['ReadonlyRootfs'] = $container->GetReadOnlySetting(); + + $exposedPorts = []; + if ($container->GetInternalPort() !== 'host') { + foreach ($container->GetPorts()->GetPorts() as $value) { + $port = $value->port; + $protocol = $value->protocol; + if ($port === '%APACHE_PORT%') { + $port = $this->configurationManager->GetApachePort(); + // Do not expose udp if AIO is in reverse proxy mode + if ($port !== '443' && $protocol === 'udp') { + continue; + } + } elseif ($port === '%TALK_PORT%') { + $port = $this->configurationManager->GetTalkPort(); + } + $portWithProtocol = $port . '/' . $protocol; + $exposedPorts[$portWithProtocol] = null; + } + $requestBody['HostConfig']['NetworkMode'] = 'nextcloud-aio'; + } else { + $requestBody['HostConfig']['NetworkMode'] = 'host'; + } + + if (count($exposedPorts) > 0) { + $requestBody['ExposedPorts'] = $exposedPorts; + foreach ($container->GetPorts()->GetPorts() as $value) { + $port = $value->port; + $protocol = $value->protocol; + if ($port === '%APACHE_PORT%') { + $port = $this->configurationManager->GetApachePort(); + // Do not expose udp if AIO is in reverse proxy mode + if ($port !== '443' && $protocol === 'udp') { + continue; + } + } elseif ($port === '%TALK_PORT%') { + $port = $this->configurationManager->GetTalkPort(); + } + $ipBinding = $value->ipBinding; + if ($ipBinding === '%APACHE_IP_BINDING%') { + $ipBinding = $this->configurationManager->GetApacheIPBinding(); + // Do not expose if AIO is in internal network mode + if ($ipBinding === '@INTERNAL') { + continue; + } + } + $portWithProtocol = $port . '/' . $protocol; + $requestBody['HostConfig']['PortBindings'][$portWithProtocol] = [ + [ + 'HostPort' => $port, + 'HostIp' => $ipBinding, + ] + ]; + } + } + + $devices = []; + foreach ($container->GetDevices() as $device) { + if ($device === '/dev/dri' && ! $this->configurationManager->isDriDeviceEnabled()) { + continue; + } + $devices[] = ['PathOnHost' => $device, 'PathInContainer' => $device, 'CgroupPermissions' => 'rwm']; + } + + if (count($devices) > 0) { + $requestBody['HostConfig']['Devices'] = $devices; + } + + $shmSize = $container->GetShmSize(); + if ($shmSize > 0) { + $requestBody['HostConfig']['ShmSize'] = $shmSize; + } + + $tmpfs = []; + foreach ($container->GetTmpfs() as $tmp) { + $mode = ''; + if (str_contains($tmp, ':')) { + $mode = explode(':', $tmp)[1]; + $tmp = explode(':', $tmp)[0]; + } + $tmpfs[$tmp] = $mode; + } + if (count($tmpfs) > 0) { + $requestBody['HostConfig']['Tmpfs'] = $tmpfs; + } + + $requestBody['HostConfig']['Init'] = $container->GetInit(); + + $capAdds = $container->GetCapAdds(); + if (count($capAdds) > 0) { + $requestBody['HostConfig']['CapAdd'] = $capAdds; + } + + // Disable arp spoofing + if (!in_array('NET_RAW', $capAdds, true)) { + $requestBody['HostConfig']['CapDrop'] = ['NET_RAW']; + } + + // Disable SELinux for AIO containers so that it does not break them + $requestBody['HostConfig']['SecurityOpt'] = ['label:disable']; + if ($container->isApparmorUnconfined()) { + $requestBody['HostConfig']['SecurityOpt'] = ['apparmor:unconfined', 'label:disable']; + } + + $mounts = []; + + // Special things for the backup container which should not be exposed in the containers.json + if ($container->GetIdentifier() === 'nextcloud-aio-borgbackup') { + // Additional backup directories + foreach ($this->getAllBackupVolumes() as $additionalBackupVolumes) { + if ($additionalBackupVolumes !== '') { + $mounts[] = ['Type' => 'volume', 'Source' => $additionalBackupVolumes, 'Target' => '/nextcloud_aio_volumes/' . $additionalBackupVolumes, 'ReadOnly' => false]; + } + } + foreach ($this->configurationManager->GetAdditionalBackupDirectoriesArray() as $additionalBackupDirectories) { + if ($additionalBackupDirectories !== '') { + if (!str_starts_with($additionalBackupDirectories, '/')) { + $mounts[] = ['Type' => 'volume', 'Source' => $additionalBackupDirectories, 'Target' => '/docker_volumes/' . $additionalBackupDirectories, 'ReadOnly' => true]; + } else { + $mounts[] = ['Type' => 'bind', 'Source' => $additionalBackupDirectories, 'Target' => '/host_mounts' . $additionalBackupDirectories, 'ReadOnly' => true, 'BindOptions' => ['NonRecursive' => true]]; + } + } + } + // Special things for the talk container which should not be exposed in the containers.json + } elseif ($container->GetIdentifier() === 'nextcloud-aio-talk') { + // This is needed due to a bug in libwebsockets which cannot handle unlimited ulimits + $requestBody['HostConfig']['Ulimits'] = [['Name' => 'nofile', 'Hard' => 200000, 'Soft' => 200000]]; + // // Special things for the nextcloud container which should not be exposed in the containers.json + // } elseif ($container->GetIdentifier() === 'nextcloud-aio-nextcloud') { + // foreach ($container->GetVolumes()->GetVolumes() as $volume) { + // if ($volume->name !== $this->configurationManager->GetNextcloudMount()) { + // continue; + // } + // $mounts[] = ["Type" => "bind", "Source" => $volume->name, "Target" => $volume->mountPoint, "ReadOnly" => !$volume->isWritable, "BindOptions" => [ "Propagation" => "rshared"]]; + // } + // Special things for the caddy community container + } elseif ($container->GetIdentifier() === 'nextcloud-aio-caddy') { + $requestBody['HostConfig']['ExtraHosts'] = ['host.docker.internal:host-gateway']; + } + + if (count($mounts) > 0) { + $requestBody['HostConfig']['Mounts'] = $mounts; + } + + $url = $this->BuildApiUrl('containers/create?name=' . $container->GetIdentifier()); + try { + $this->guzzleClient->request( + 'POST', + $url, + [ + 'json' => $requestBody + ] + ); + } catch (RequestException $e) { + throw new \Exception('Could not create container ' . $container->GetIdentifier() . ': ' . $e->getMessage()); + } + + } + + public function isDockerHubReachable(Container $container) : bool { + $tag = $container->GetImageTag(); + if ($tag === '%AIO_CHANNEL%') { + $tag = $this->GetCurrentChannel(); + } + + $remoteDigest = $this->dockerHubManager->GetLatestDigestOfTag($container->GetContainerName(), $tag); + + if ($remoteDigest === null) { + return false; + } else { + return true; + } + } + + public function PullImage(Container $container) : void { + $imageName = $this->BuildImageName($container); + $encodedImageName = urlencode($imageName); + $url = $this->BuildApiUrl(sprintf('images/create?fromImage=%s', $encodedImageName)); + $imageIsThere = true; + try { + $imageUrl = $this->BuildApiUrl(sprintf('images/%s/json', $encodedImageName)); + $this->guzzleClient->get($imageUrl)->getBody()->getContents(); + } catch (\Throwable $e) { + $imageIsThere = false; + } + try { + $this->guzzleClient->post($url); + } catch (RequestException $e) { + if ($imageIsThere === false) { + throw new \Exception('Could not pull image ' . $imageName . ". Please run 'sudo docker exec -it nextcloud-aio-mastercontainer docker pull " . $imageName . "' in order to find out why it failed."); + } + } + } + + private function isContainerUpdateAvailable(string $id) : string { + $container = $this->containerDefinitionFetcher->GetContainerById($id); + + $updateAvailable = ''; + if ($container->GetUpdateState() instanceof VersionDifferentState) { + $updateAvailable = '1'; + } + foreach ($container->GetDependsOn() as $dependency) { + $updateAvailable .= $this->isContainerUpdateAvailable($dependency); + } + return $updateAvailable; + } + + public function isAnyUpdateAvailable() : bool { + // return early if instance is not installed + if (!$this->configurationManager->wasStartButtonClicked()) { + return false; + } + $id = 'nextcloud-aio-apache'; + + if ($this->isContainerUpdateAvailable($id) !== '') { + return true; + } else { + return false; + } + } + + private function getBackupVolumes(string $id) : string { + $container = $this->containerDefinitionFetcher->GetContainerById($id); + + $backupVolumes = ''; + foreach ($container->GetBackupVolumes() as $backupVolume) { + $backupVolumes .= $backupVolume . ' '; + } + foreach ($container->GetDependsOn() as $dependency) { + $backupVolumes .= $this->getBackupVolumes($dependency); + } + return $backupVolumes; + } + + private function getAllBackupVolumes() : array { + $id = 'nextcloud-aio-apache'; + $backupVolumesArray = explode(' ', $this->getBackupVolumes($id)); + return array_unique($backupVolumesArray); + } + + private function GetNextcloudExecCommands(string $id) : string { + $container = $this->containerDefinitionFetcher->GetContainerById($id); + + $nextcloudExecCommands = ''; + foreach ($container->GetNextcloudExecCommands() as $execCommand) { + $nextcloudExecCommands .= $execCommand . PHP_EOL; + } + foreach ($container->GetDependsOn() as $dependency) { + $nextcloudExecCommands .= $this->GetNextcloudExecCommands($dependency); + } + return $nextcloudExecCommands; + } + + private function GetAllNextcloudExecCommands() : string { + $id = 'nextcloud-aio-apache'; + return 'NEXTCLOUD_EXEC_COMMANDS=' . $this->GetNextcloudExecCommands($id); + } + + private function GetRepoDigestsOfContainer(string $containerName) : ?array { + try { + $containerUrl = $this->BuildApiUrl(sprintf('containers/%s/json', $containerName)); + $containerOutput = json_decode($this->guzzleClient->get($containerUrl)->getBody()->getContents(), true); + $imageName = $containerOutput['Image']; + + $imageUrl = $this->BuildApiUrl(sprintf('images/%s/json', $imageName)); + $imageOutput = json_decode($this->guzzleClient->get($imageUrl)->getBody()->getContents(), true); + + if (!isset($imageOutput['RepoDigests'])) { + error_log('RepoDigests is not set of container ' . $containerName); + return null; + } + + if (!is_array($imageOutput['RepoDigests'])) { + error_log('RepoDigests of ' . $containerName . ' is not an array which is not allowed!'); + return null; + } + + $repoDigestArray = []; + $oneDigestGiven = false; + foreach ($imageOutput['RepoDigests'] as $repoDigest) { + $digestPosition = strpos($repoDigest, '@'); + if ($digestPosition === false) { + error_log('Somehow the RepoDigest of ' . $containerName . ' does not contain a @.'); + return null; + } + $repoDigestArray[] = substr($repoDigest, $digestPosition + 1); + $oneDigestGiven = true; + } + + if ($oneDigestGiven) { + return $repoDigestArray; + } + + return null; + } catch (\Exception $e) { + return null; + } + } + + public function GetCurrentChannel() : string { + $cacheKey = 'aio-ChannelName'; + $channelName = apcu_fetch($cacheKey); + if ($channelName !== false && is_string($channelName)) { + return $channelName; + } + + $containerName = 'nextcloud-aio-mastercontainer'; + $url = $this->BuildApiUrl(sprintf('containers/%s/json', $containerName)); + try { + $output = json_decode($this->guzzleClient->get($url)->getBody()->getContents(), true); + $containerChecksum = $output['Image']; + $tagArray = explode(':', $output['Config']['Image']); + $tag = $tagArray[1]; + apcu_add($cacheKey, $tag); + /** + * @psalm-suppress TypeDoesNotContainNull + * @psalm-suppress DocblockTypeContradiction + */ + if ($tag === null) { + error_log("No tag was found when getting the current channel. You probably did not follow the documentation correctly. Changing the channel to the default 'latest'."); + $tag = 'latest'; + } + return $tag; + } catch (\Exception $e) { + error_log('Could not get current channel ' . $e->getMessage()); + } + + return 'latest'; + } + + public function IsMastercontainerUpdateAvailable() : bool { + $imageName = 'nextcloud/all-in-one'; + $containerName = 'nextcloud-aio-mastercontainer'; + + $tag = $this->GetCurrentChannel(); + + $runningDigests = $this->GetRepoDigestsOfContainer($containerName); + if ($runningDigests === null) { + return true; + } + $remoteDigest = $this->dockerHubManager->GetLatestDigestOfTag($imageName, $tag); + if ($remoteDigest === null) { + return false; + } + + foreach ($runningDigests as $runningDigest) { + if ($remoteDigest === $runningDigest) { + return false; + } + } + return true; + } + + public function sendNotification(Container $container, string $subject, string $message, string $file = '/notify.sh') : void { + if ($this->GetContainerStartingState($container) instanceof RunningState) { + + $containerName = $container->GetIdentifier(); + + // schedule the exec + $url = $this->BuildApiUrl(sprintf('containers/%s/exec', urlencode($containerName))); + $response = json_decode( + $this->guzzleClient->request( + 'POST', + $url, + [ + 'json' => [ + 'AttachStdout' => true, + 'Tty' => true, + 'Cmd' => [ + 'bash', + $file, + $subject, + $message + ], + ], + ] + )->getBody()->getContents(), + true + ); + + $id = $response['Id']; + + // start the exec + $url = $this->BuildApiUrl(sprintf('exec/%s/start', $id)); + $this->guzzleClient->request( + 'POST', + $url, + [ + 'json' => [ + 'Detach' => false, + 'Tty' => true, + ], + ] + ); + } + } + + private function DisconnectContainerFromBridgeNetwork(string $id) : void { + + $url = $this->BuildApiUrl( + sprintf('networks/%s/disconnect', 'bridge') + ); + + try { + $this->guzzleClient->request( + 'POST', + $url, + [ + 'json' => [ + 'container' => $id, + ], + ] + ); + } catch (RequestException $e) { + } + } + + private function ConnectContainerIdToNetwork(string $id, string $internalPort, string $network = 'nextcloud-aio') : void { + if ($internalPort === 'host') { + return; + } + + $url = $this->BuildApiUrl('networks/create'); + try { + $this->guzzleClient->request( + 'POST', + $url, + [ + 'json' => [ + 'Name' => $network, + 'CheckDuplicate' => true, + 'Driver' => 'bridge', + 'Internal' => false, + ] + ] + ); + } catch (RequestException $e) { + // 409 is undocumented and gets thrown if the network already exists. + if ($e->getCode() !== 409) { + throw new \Exception('Could not create the nextcloud-aio network: ' . $e->getMessage()); + } + } + + $url = $this->BuildApiUrl( + sprintf('networks/%s/connect', $network) + ); + try { + $this->guzzleClient->request( + 'POST', + $url, + [ + 'json' => [ + 'container' => $id, + ] + ] + ); + } catch (RequestException $e) { + // 403 is undocumented and gets thrown if a specific container is already part of a network + if ($e->getCode() !== 403) { + throw $e; + } + } + } + + public function ConnectMasterContainerToNetwork() : void { + $this->ConnectContainerIdToNetwork('nextcloud-aio-mastercontainer', ''); + // Don't disconnect here since it slows down the initial login by a lot. Is getting done during cron.sh instead. + // $this->DisconnectContainerFromBridgeNetwork('nextcloud-aio-mastercontainer'); + } + + public function ConnectContainerToNetwork(Container $container) : void { + $this->ConnectContainerIdToNetwork($container->GetIdentifier(), $container->GetInternalPort()); + } + + public function StopContainer(Container $container) : void { + $url = $this->BuildApiUrl(sprintf('containers/%s/stop?t=%s', urlencode($container->GetIdentifier()), $container->GetMaxShutdownTime())); + try { + $this->guzzleClient->post($url); + } catch (RequestException $e) { + if ($e->getCode() !== 404 && $e->getCode() !== 304) { + throw $e; + } + } + } + + public function GetBackupcontainerExitCode() : int { + $containerName = 'nextcloud-aio-borgbackup'; + $url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($containerName))); + try { + $response = $this->guzzleClient->get($url); + } catch (RequestException $e) { + if ($e->getCode() === 404) { + return -1; + } + throw $e; + } + + $responseBody = json_decode((string)$response->getBody(), true); + + $exitCode = $responseBody['State']['ExitCode']; + if (is_int($exitCode)) { + return $exitCode; + } else { + return -1; + } + } + + public function GetDatabasecontainerExitCode() : int { + $containerName = 'nextcloud-aio-database'; + $url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($containerName))); + try { + $response = $this->guzzleClient->get($url); + } catch (RequestException $e) { + if ($e->getCode() === 404) { + return -1; + } + throw $e; + } + + $responseBody = json_decode((string)$response->getBody(), true); + + $exitCode = $responseBody['State']['ExitCode']; + if (is_int($exitCode)) { + return $exitCode; + } else { + return -1; + } + } + + public function isLoginAllowed() : bool { + $id = 'nextcloud-aio-apache'; + $apacheContainer = $this->containerDefinitionFetcher->GetContainerById($id); + if ($this->GetContainerStartingState($apacheContainer) instanceof RunningState) { + return false; + } + return true; + } + + public function isBackupContainerRunning() : bool { + $id = 'nextcloud-aio-borgbackup'; + $backupContainer = $this->containerDefinitionFetcher->GetContainerById($id); + if ($this->GetContainerRunningState($backupContainer) instanceof RunningState) { + return true; + } + return false; + } + + private function GetCreatedTimeOfNextcloudImage() : ?string { + $imageName = 'nextcloud/aio-nextcloud' . ':' . $this->GetCurrentChannel(); + try { + $imageUrl = $this->BuildApiUrl(sprintf('images/%s/json', $imageName)); + $imageOutput = json_decode($this->guzzleClient->get($imageUrl)->getBody()->getContents(), true); + + if (!isset($imageOutput['Created'])) { + error_log('Created is not set of image ' . $imageName); + return null; + } + + return str_replace('T', ' ', (string)$imageOutput['Created']); + } catch (\Exception $e) { + return null; + } + } + + public function isNextcloudImageOutdated() : bool { + $createdTime = $this->GetCreatedTimeOfNextcloudImage(); + + if ($createdTime === null) { + return false; + } + + // If the image is older than 90 days, it is outdated. + if ((time() - (60 * 60 * 24 * 90)) > strtotime($createdTime)) { + return true; + } + + return false; + } } diff --git a/php/src/Docker/DockerHubManager.php b/php/src/Docker/DockerHubManager.php index 36f46982fa2..ddca9968019 100644 --- a/php/src/Docker/DockerHubManager.php +++ b/php/src/Docker/DockerHubManager.php @@ -2,61 +2,57 @@ namespace AIO\Docker; -use AIO\ContainerDefinitionFetcher; -use AIO\Data\ConfigurationManager; use GuzzleHttp\Client; -class DockerHubManager -{ - private Client $guzzleClient; - - public function __construct() - { - $this->guzzleClient = new Client(); - } - - public function GetLatestDigestOfTag(string $name, string $tag) : ?string { - $cacheKey = 'dockerhub-manifest-' . $name . $tag; - - $cachedVersion = apcu_fetch($cacheKey); - if($cachedVersion !== false && is_string($cachedVersion)) { - return $cachedVersion; - } - - // If one of the links below should ever become outdated, we can still upgrade the mastercontainer via the webinterface manually by opening '/api/docker/getwatchtower' - - try { - $authTokenRequest = $this->guzzleClient->request( - 'GET', - 'https://auth.docker.io/token?service=registry.docker.io&scope=repository:' . $name . ':pull' - ); - $body = $authTokenRequest->getBody()->getContents(); - $decodedBody = json_decode($body, true); - if(isset($decodedBody['token'])) { - $authToken = $decodedBody['token']; - $manifestRequest = $this->guzzleClient->request( - 'HEAD', - 'https://registry-1.docker.io/v2/'.$name.'/manifests/' . $tag, - [ - 'headers' => [ - 'Accept' => 'application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.docker.distribution.manifest.v2+json', - 'Authorization' => 'Bearer ' . $authToken, - ], - ] - ); - $responseHeaders = $manifestRequest->getHeader('docker-content-digest'); - if(count($responseHeaders) === 1) { - $latestVersion = $responseHeaders[0]; - apcu_add($cacheKey, $latestVersion, 600); - return $latestVersion; - } - } - - error_log('Could not get digest of container ' . $name . ':' . $tag); - return null; - } catch (\Exception $e) { - error_log('Could not get digest of container ' . $name . ':' . $tag . ' ' . $e->getMessage()); - return null; - } - } -} \ No newline at end of file +class DockerHubManager { + private Client $guzzleClient; + + public function __construct() { + $this->guzzleClient = new Client(); + } + + public function GetLatestDigestOfTag(string $name, string $tag) : ?string { + $cacheKey = 'dockerhub-manifest-' . $name . $tag; + + $cachedVersion = apcu_fetch($cacheKey); + if ($cachedVersion !== false && is_string($cachedVersion)) { + return $cachedVersion; + } + + // If one of the links below should ever become outdated, we can still upgrade the mastercontainer via the webinterface manually by opening '/api/docker/getwatchtower' + + try { + $authTokenRequest = $this->guzzleClient->request( + 'GET', + 'https://auth.docker.io/token?service=registry.docker.io&scope=repository:' . $name . ':pull' + ); + $body = $authTokenRequest->getBody()->getContents(); + $decodedBody = json_decode($body, true); + if (isset($decodedBody['token'])) { + $authToken = $decodedBody['token']; + $manifestRequest = $this->guzzleClient->request( + 'HEAD', + 'https://registry-1.docker.io/v2/' . $name . '/manifests/' . $tag, + [ + 'headers' => [ + 'Accept' => 'application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.docker.distribution.manifest.v2+json', + 'Authorization' => 'Bearer ' . $authToken, + ], + ] + ); + $responseHeaders = $manifestRequest->getHeader('docker-content-digest'); + if (count($responseHeaders) === 1) { + $latestVersion = $responseHeaders[0]; + apcu_add($cacheKey, $latestVersion, 600); + return $latestVersion; + } + } + + error_log('Could not get digest of container ' . $name . ':' . $tag); + return null; + } catch (\Exception $e) { + error_log('Could not get digest of container ' . $name . ':' . $tag . ' ' . $e->getMessage()); + return null; + } + } +} diff --git a/php/src/Middleware/AuthMiddleware.php b/php/src/Middleware/AuthMiddleware.php index c0c814b838b..a54b0104918 100644 --- a/php/src/Middleware/AuthMiddleware.php +++ b/php/src/Middleware/AuthMiddleware.php @@ -8,34 +8,32 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; -class AuthMiddleware -{ - private AuthManager $authManager; +class AuthMiddleware { + private AuthManager $authManager; - public function __construct(AuthManager $authManager) { - $this->authManager = $authManager; - } + public function __construct(AuthManager $authManager) { + $this->authManager = $authManager; + } - public function __invoke(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface - { - $publicRoutes = [ - '/api/auth/login', - '/api/auth/getlogin', - '/login', - '/setup', - '/', - ]; + public function __invoke(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { + $publicRoutes = [ + '/api/auth/login', + '/api/auth/getlogin', + '/login', + '/setup', + '/', + ]; - if(!in_array($request->getUri()->getPath(), $publicRoutes)) { - if(!$this->authManager->IsAuthenticated()) { - $status = 302; - $headers = ['Location' => '/']; - $response = new Response($status, $headers); - return $response; - } - } + if (!in_array($request->getUri()->getPath(), $publicRoutes)) { + if (!$this->authManager->IsAuthenticated()) { + $status = 302; + $headers = ['Location' => '/']; + $response = new Response($status, $headers); + return $response; + } + } - $response = $handler->handle($request); - return $response; - } + $response = $handler->handle($request); + return $response; + } } diff --git a/php/src/Twig/ClassExtension.php b/php/src/Twig/ClassExtension.php index ff5ffe44b04..7e66a4b7beb 100644 --- a/php/src/Twig/ClassExtension.php +++ b/php/src/Twig/ClassExtension.php @@ -5,21 +5,18 @@ use Slim\Views\TwigExtension; use Twig\TwigFunction; -class ClassExtension extends TwigExtension -{ - public function getFunctions() : array - { - return array( - new TwigFunction('class', array($this, 'getClassName')), - ); - } +class ClassExtension extends TwigExtension { + public function getFunctions() : array { + return [ + new TwigFunction('class', [$this, 'getClassName']), + ]; + } - public function getClassName(mixed $object) : ?string - { - if (!is_object($object)) { - return null; - } + public function getClassName(mixed $object) : ?string { + if (!is_object($object)) { + return null; + } - return get_class($object); - } -} \ No newline at end of file + return get_class($object); + } +} diff --git a/php/src/Twig/CsrfExtension.php b/php/src/Twig/CsrfExtension.php index 6df974aebc4..69e6b1eec5b 100644 --- a/php/src/Twig/CsrfExtension.php +++ b/php/src/Twig/CsrfExtension.php @@ -4,35 +4,32 @@ use Slim\Csrf\Guard; -class CsrfExtension extends \Twig\Extension\AbstractExtension implements \Twig\Extension\GlobalsInterface -{ - /** - * @var Guard - */ - protected Guard $csrf; +class CsrfExtension extends \Twig\Extension\AbstractExtension implements \Twig\Extension\GlobalsInterface { + /** + * @var Guard + */ + protected Guard $csrf; - public function __construct(Guard $csrf) - { - $this->csrf = $csrf; - } + public function __construct(Guard $csrf) { + $this->csrf = $csrf; + } - public function getGlobals() : array - { - // CSRF token name and value - $csrfNameKey = $this->csrf->getTokenNameKey(); - $csrfValueKey = $this->csrf->getTokenValueKey(); - $csrfName = $this->csrf->getTokenName(); - $csrfValue = $this->csrf->getTokenValue(); + public function getGlobals() : array { + // CSRF token name and value + $csrfNameKey = $this->csrf->getTokenNameKey(); + $csrfValueKey = $this->csrf->getTokenValueKey(); + $csrfName = $this->csrf->getTokenName(); + $csrfValue = $this->csrf->getTokenValue(); - return [ - 'csrf' => [ - 'keys' => [ - 'name' => $csrfNameKey, - 'value' => $csrfValueKey - ], - 'name' => $csrfName, - 'value' => $csrfValue - ] - ]; - } -} \ No newline at end of file + return [ + 'csrf' => [ + 'keys' => [ + 'name' => $csrfNameKey, + 'value' => $csrfValueKey + ], + 'name' => $csrfName, + 'value' => $csrfValue + ] + ]; + } +}