alias('session', 'session.cli')->alias(\Joomla\CMS\Session\Session::class, 'session.cli')->alias(\Joomla\Session\Session::class, 'session.cli')->alias(\Joomla\Session\SessionInterface::class, 'session.cli'); $app = $container->get(\Joomla\CMS\Application\AdministratorApplication::class); \Joomla\CMS\Factory::$application = $app; $db = $container->get(\Joomla\Database\DatabaseInterface::class); $uid = (int) $db->setQuery('SELECT id FROM #__users WHERE block=0 AND username=' . $db->quote($username))->loadResult(); if (!$uid) throw new RuntimeException('Authorized Joomla account not found or blocked.'); $user = $container->get(\Joomla\CMS\User\UserFactoryInterface::class)->loadUserById($uid); if (!$user->authorise('core.login.admin')) throw new RuntimeException('Account lacks administrator login permission.'); $app->loadIdentity($user); $app->loadLanguage($container->get(\Joomla\CMS\Language\LanguageFactoryInterface::class)->createLanguage('en-GB', false)); \Joomla\CMS\Factory::$language = $app->getLanguage(); $app->getLanguage()->load('joomla', JPATH_ADMINISTRATOR); $app->createExtensionNamespaceMap(); // SP Page Builder's content plugin expects this class during CLI article saves. require_once JPATH_SITE . '/components/com_sppagebuilder/helpers/autoload.php'; $cmd = $argv[1] ?? 'help'; $backupDir = __DIR__ . '/backups'; $json = static fn($v) => json_encode($v, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); $read = static function ($file) { if (!$file || !is_file($file)) throw new RuntimeException('JSON file not found.'); return json_decode(file_get_contents($file), true, 512, JSON_THROW_ON_ERROR); }; $hash = static function (array $r) { unset($r['hits'], $r['checked_out'], $r['checked_out_time']); return hash('sha256', json_encode($r, JSON_THROW_ON_ERROR)); }; $backup = static function (string $kind, array $r) use ($backupDir, $json): string { if (!is_dir($backupDir) && !mkdir($backupDir, 0700, true)) throw new RuntimeException('Cannot create private backup folder.'); $f = $backupDir . '/' . $kind . '-' . ($r['id'] ?? 0) . '-' . gmdate('Ymd-His') . '-' . bin2hex(random_bytes(3)) . '.json'; if (file_put_contents($f, $json($r), LOCK_EX) === false) throw new RuntimeException('Backup failed.'); return $f; }; $get = static function (string $table, int $id) use ($db): array { if ($id < 1) throw new RuntimeException('A positive ID is required.'); $r = $db->setQuery('SELECT * FROM #__' . $table . ' WHERE id=' . $id)->loadAssoc(); if (!$r) throw new RuntimeException('Record not found.'); return $r; }; $guard = static function (array $r, array $input) use ($hash): void { if (!empty($r['checked_out'])) throw new RuntimeException('Record checked out. Ask the editor to Save & Close or Cancel; do not force a check-in.'); if (!isset($input['source_hash']) || !hash_equals($hash($r), $input['source_hash'])) throw new RuntimeException('Record changed since export. Export again and merge your edits.'); }; $cleanCache = static function () use ($container): void { $factory = $container->get(\Joomla\CMS\Cache\CacheControllerFactoryInterface::class); foreach (['com_content','com_sppagebuilder','mod_ipmi_content','page'] as $group) $factory->createCacheController('callback', ['defaultgroup'=>$group])->clean(); }; if ($cmd === 'inventory') { $out = ['version'=>JVERSION, 'site'=>$site]; foreach (['categories'=>"SELECT id,title,alias,parent_id,published FROM #__categories WHERE extension='com_content'", 'articles'=>'SELECT id,title,alias,catid,state,featured,publish_up,publish_down,checked_out FROM #__content', 'pages'=>'SELECT id,title,published,checked_out FROM #__sppagebuilder', 'menus'=>"SELECT id,title,alias,link,menutype,parent_id,level FROM #__menu WHERE client_id=0 AND published=1", 'modules'=>"SELECT id,title,published,params FROM #__modules WHERE module='mod_ipmi_content'"] as $k=>$q) $out[$k]=$db->setQuery($q)->loadAssocList(); echo $json($out) . "\n"; } elseif ($cmd === 'article-export') { $r=$get('content',(int)($argv[2]??0)); $category=$db->setQuery('SELECT alias FROM #__categories WHERE id='.(int)$r['catid'])->loadResult(); $out=['id'=>(int)$r['id'],'source_hash'=>$hash($r),'category'=>$category]; foreach(['title','alias','introtext','fulltext','state','featured','access','language','created_by_alias','publish_up','publish_down','metadesc'] as $key)$out[$key]=$r[$key]; $out['images']=json_decode($r['images']?:'{}',true); $file=$argv[3]??null; if(!$file)throw new RuntimeException('Specify output JSON path.'); if(file_exists($file))throw new RuntimeException('Output exists; use a new filename.'); if(file_put_contents($file,$json($out))===false)throw new RuntimeException('Cannot write export.'); echo $json(['export'=>$file,'id'=>$r['id']])."\n"; } elseif ($cmd === 'article-save') { $input=$read($argv[2]??null); $id=(int)($input['id']??0); $r=$id?$get('content',$id):[]; if($id){$guard($r,$input);if(!$user->authorise('core.edit','com_content.article.'.$id))throw new RuntimeException('No article edit permission.');} $allowed=['ministry-news','reflections','prayer-alerts','prayer-resources']; if(!in_array($input['category']??'', $allowed,true))throw new RuntimeException('Use a documented category alias. Daily Devotions uses reflections.'); $cat=(int)$db->setQuery("SELECT id FROM #__categories WHERE extension='com_content' AND alias=".$db->quote($input['category']))->loadResult(); if(!$cat)throw new RuntimeException('Category not found.'); if(!$id&&!$user->authorise('core.create','com_content.category.'.$cat))throw new RuntimeException('No article create permission.'); $state=(int)($input['state']??($r['state']??0)); if(!in_array($state,[-2,0,1,2],true))throw new RuntimeException('state must be -2, 0, 1, or 2.'); if(($state!==0 || ($id&&$state!==(int)$r['state']) || array_key_exists('featured',$input))&&!$user->authorise('core.edit.state','com_content.category.'.$cat))throw new RuntimeException('No publishing permission.'); foreach(['title','alias'] as $key)if(empty($input[$key]))throw new RuntimeException($key.' is required.'); if(!preg_match('/^[a-z0-9]+(?:-[a-z0-9]+)*$/',$input['alias']))throw new RuntimeException('Use a lowercase, hyphenated URL alias.'); if($db->setQuery('SELECT id FROM #__content WHERE catid='.$cat.' AND alias='.$db->quote($input['alias']).' AND id<>'.$id)->loadResult())throw new RuntimeException('Alias already exists. Export and update its ID instead.'); $data=$r; foreach(['title','alias','introtext','fulltext','created_by_alias','metadesc','publish_up','publish_down','access','language','featured'] as $key)if(array_key_exists($key,$input))$data[$key]=$input[$key]; foreach(['publish_up','publish_down'] as $key)if(!empty($data[$key])){ $dt=DateTimeImmutable::createFromFormat('!Y-m-d H:i:s',$data[$key],new DateTimeZone('UTC')); if(!$dt||$dt->format('Y-m-d H:i:s')!==$data[$key])throw new RuntimeException($key.' must be UTC YYYY-MM-DD HH:MM:SS or null.'); } if(!empty($data['publish_up'])&&!empty($data['publish_down'])&&$data['publish_down']<=$data['publish_up'])throw new RuntimeException('Finish date must follow start date.'); $data+=['created_by'=>$uid,'created'=>gmdate('Y-m-d H:i:s'),'created_by_alias'=>'IPMI Editorial','access'=>1,'language'=>'*','introtext'=>'','fulltext'=>'','featured'=>0,'attribs'=>'{}','metadata'=>'{}','images'=>'{}']; if(isset($input['images'])){ foreach(['image_intro','image_fulltext'] as $key)if(!empty($input['images'][$key])){ $path=$input['images'][$key];$resolved=realpath($site.'/'.$path); if(!str_starts_with($path,'images/')||!$resolved||!str_starts_with($resolved,$site.'/images/'))throw new RuntimeException('Upload images under Joomla images/ before saving.'); } $data['images']=json_encode($input['images'],JSON_THROW_ON_ERROR); } $data['id']=$id;$data['catid']=$cat;$data['state']=$state; $data['articletext']=$data['introtext'].($data['fulltext']!==''?'